From b507807b6803c04f7151892412b43b2608cb444c Mon Sep 17 00:00:00 2001 From: Daniel Hazelton Date: Thu, 5 Nov 2026 10:00:31 -0500 Subject: [PATCH 1/9] make sure the slug and project ID for CF are present and correct --- gradle.properties | 4 ++-- update.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index bee48734..69c3e023 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,9 +9,9 @@ mod_description = This mod provides a complete JSON-based configuration system a mod_group = zone.moddev.mc.lib mod_authors = Cyanobacterium (Dr. Cyano), jriwanek, Mistress Curle, DShadowWolf, Gigaherz mod_license = LGPL-2.1-only -# CurseForge project ID for release uploads; leave blank to skip CurseForge publishing. (however if you do this and forget, you will have to manually publish files to CurseForge whenever you make a tag to release, so this is not advised) +# CurseForge project ID for release uploads; leave blank to skip CurseForge publishing. curseforge_id = 261744 -display_url = https://github.com/MinecraftModDevelopmentMods/MMDLib +display_url = https://www.curseforge.com/minecraft/mc-mods/mmdlib update_url = https://raw.githubusercontent.com/MinecraftModDevelopmentMods/MMDLib/refs/heads/master-2.0-1.21.1-neo/update.json issue_url = https://github.com/MinecraftModDevelopmentMods/MMDLib/issues diff --git a/update.json b/update.json index 542fe28c..45453d3b 100644 --- a/update.json +++ b/update.json @@ -1,4 +1,4 @@ { - "homepage": "https://github.com/MinecraftModDevelopmentMods/MMDLib", + "homepage": "https://www.curseforge.com/minecraft/mc-mods/mmdlib", "promos": {} } From da3e648f4872640f1cc23097404d61675582322a Mon Sep 17 00:00:00 2001 From: Daniel Hazelton Date: Sat, 7 Nov 2026 12:06:11 -0500 Subject: [PATCH 2/9] Add CraftTweaker integration for script-defined content Optional CraftTweaker support: #loader mmdlib scripts run once at startup, before registry events, and define materials, standalone blocks and items, and new block and item modules (mods.mmdlib.MaterialBuilder, BlockBuilder, BlockModule, ItemBuilder, ItemModule). All CraftTweaker code lives in compat/CraftTweaker; MMDLib's constructor reaches it through one call, made only when CraftTweaker is loaded. Generated resources: an always-enabled pack supplies blockstates, models, names, loot tables and tags for script content. Material textures are blank templates (derived from Base Metals art by scripts/generate_material_templates.py) multiplied by the material color; block and item textures come from scripts/mmdlibdata, which is loaded as an override pack above the generated one. Core changes the integration needs: - MaterialExtensions.registerDefinitionSource: code-built materials, standalone blocks and items, planned with the same checks as files. Sources run after file modules install and before material files decode, so files can use modules a source registers. - MaterialRuntimeSettings.tinted (default true): precolored materials skip the client tint. - CraftTweaker as a compile-only, optional dependency with its annotation processor. Co-Authored-By: Claude Opus 5.5 --- build.gradle | 17 +- docs/crafttweaker.md | 299 +++++++++ docs/definition-loading.md | 19 +- gradle.properties | 5 + scripts/generate_material_templates.py | 409 ++++++++++++ src/main/java/zone/moddev/mc/lib/MMDLib.java | 4 + .../moddev/mc/lib/api/MaterialExtensions.java | 76 +++ .../mc/lib/client/RuntimeContentClient.java | 7 +- .../lib/common/loading/DefinitionLoader.java | 138 +++- .../common/types/MaterialRuntimeSettings.java | 41 +- .../compat/CraftTweaker/CraftTweakerInit.java | 26 + .../MMDLibCraftTweakerPlugin.java | 34 + .../compat/CraftTweaker/MaterialPacks.java | 215 +++++++ .../CraftTweaker/MaterialTemplates.java | 46 ++ .../CraftTweaker/ScriptBlockBuilder.java | 137 ++++ .../CraftTweaker/ScriptBlockModule.java | 121 ++++ .../CraftTweaker/ScriptBlockProperties.java | 236 +++++++ .../lib/compat/CraftTweaker/ScriptData.java | 108 ++++ .../ScriptDefinitionProperties.java | 151 +++++ .../CraftTweaker/ScriptDefinitions.java | 177 +++++ .../CraftTweaker/ScriptItemBuilder.java | 103 +++ .../compat/CraftTweaker/ScriptItemModule.java | 106 +++ .../CraftTweaker/ScriptItemProperties.java | 107 ++++ .../CraftTweaker/ScriptMaterialBuilder.java | 340 ++++++++++ .../CraftTweaker/ScriptMaterialResources.java | 603 ++++++++++++++++++ .../resources/META-INF/neoforge.mods.toml | 8 + .../blockstates/__material___anvil.json | 49 ++ .../mmdlib/blockstates/__material___bars.json | 100 +++ .../blockstates/__material___block.json | 7 + .../blockstates/__material___bookshelf.json | 7 + .../blockstates/__material___button.json | 118 ++++ .../mmdlib/blockstates/__material___door.json | 124 ++++ .../blockstates/__material___double_slab.json | 7 + .../blockstates/__material___endore.json | 7 + .../blockstates/__material___fence.json | 48 ++ .../blockstates/__material___fence_gate.json | 80 +++ .../blockstates/__material___flower_pot.json | 7 + .../blockstates/__material___ladder.json | 19 + .../blockstates/__material___lever.json | 110 ++++ .../blockstates/__material___molten.json | 7 + .../blockstates/__material___netherore.json | 7 + .../mmdlib/blockstates/__material___ore.json | 7 + .../blockstates/__material___plate.json | 22 + .../__material___pressure_plate.json | 10 + .../mmdlib/blockstates/__material___slab.json | 13 + .../blockstates/__material___stairs.json | 209 ++++++ .../blockstates/__material___trapdoor.json | 58 ++ .../__material___tripwire_hook.json | 64 ++ .../mmdlib/blockstates/__material___wall.json | 90 +++ .../__material___weighted_pressure_plate.json | 52 ++ .../models/block/__material___anvil.json | 8 + .../block/__material___anvil_chipped.json | 8 + .../block/__material___anvil_damaged.json | 8 + .../models/block/__material___bars_cap.json | 9 + .../block/__material___bars_cap_alt.json | 9 + .../models/block/__material___bars_post.json | 8 + .../block/__material___bars_post_ends.json | 8 + .../models/block/__material___bars_side.json | 9 + .../block/__material___bars_side_alt.json | 9 + .../models/block/__material___block.json | 6 + .../models/block/__material___bookshelf.json | 6 + .../models/block/__material___button.json | 6 + .../block/__material___button_inventory.json | 6 + .../block/__material___button_pressed.json | 6 + .../block/__material___door_bottom_left.json | 8 + .../__material___door_bottom_left_open.json | 8 + .../block/__material___door_bottom_right.json | 8 + .../__material___door_bottom_right_open.json | 8 + .../block/__material___door_top_left.json | 8 + .../__material___door_top_left_open.json | 8 + .../block/__material___door_top_right.json | 8 + .../__material___door_top_right_open.json | 8 + .../models/block/__material___endore.json | 87 +++ .../models/block/__material___fence_gate.json | 6 + .../block/__material___fence_gate_open.json | 6 + .../block/__material___fence_gate_wall.json | 6 + .../__material___fence_gate_wall_open.json | 6 + .../block/__material___fence_inventory.json | 6 + .../models/block/__material___fence_post.json | 6 + .../models/block/__material___fence_side.json | 6 + .../models/block/__material___flower_pot.json | 8 + .../models/block/__material___ladder.json | 8 + .../models/block/__material___lever.json | 8 + .../models/block/__material___lever_on.json | 8 + .../models/block/__material___netherore.json | 87 +++ .../mmdlib/models/block/__material___ore.json | 87 +++ .../models/block/__material___plate_down.json | 41 ++ .../models/block/__material___plate_east.json | 41 ++ .../block/__material___plate_north.json | 41 ++ .../block/__material___plate_south.json | 41 ++ .../models/block/__material___plate_up.json | 41 ++ .../models/block/__material___plate_west.json | 41 ++ .../block/__material___pressure_plate.json | 6 + .../__material___pressure_plate_down.json | 6 + .../models/block/__material___slab.json | 8 + .../block/__material___slab_double.json | 6 + .../models/block/__material___slab_top.json | 8 + .../models/block/__material___stairs.json | 8 + .../block/__material___stairs_inner.json | 8 + .../block/__material___stairs_outer.json | 8 + .../block/__material___trapdoor_bottom.json | 7 + .../block/__material___trapdoor_open.json | 7 + .../block/__material___trapdoor_top.json | 7 + .../block/__material___tripwire_hook.json | 8 + .../__material___tripwire_hook_attached.json | 8 + ..._material___tripwire_hook_attached_on.json | 8 + .../block/__material___tripwire_hook_on.json | 8 + .../block/__material___wall_inventory.json | 6 + .../models/block/__material___wall_post.json | 6 + .../models/block/__material___wall_side.json | 6 + .../block/__material___wall_side_tall.json | 6 + .../__material___weighted_pressure_plate.json | 6 + ...terial___weighted_pressure_plate_down.json | 6 + .../models/item/__material___anvil.json | 3 + .../models/item/__material___arrow.json | 6 + .../mmdlib/models/item/__material___axe.json | 6 + .../mmdlib/models/item/__material___bars.json | 6 + .../models/item/__material___blend.json | 6 + .../models/item/__material___block.json | 3 + .../mmdlib/models/item/__material___bolt.json | 6 + .../models/item/__material___bookshelf.json | 3 + .../models/item/__material___boots.json | 6 + .../mmdlib/models/item/__material___bow.json | 28 + .../item/__material___bow_pulling_0.json | 6 + .../item/__material___bow_pulling_1.json | 6 + .../item/__material___bow_pulling_2.json | 6 + .../models/item/__material___button.json | 3 + .../models/item/__material___chestplate.json | 6 + .../models/item/__material___clump.json | 6 + .../models/item/__material___crackhammer.json | 6 + .../models/item/__material___crossbow.json | 41 ++ .../item/__material___crossbow_arrow.json | 6 + .../item/__material___crossbow_firework.json | 6 + .../item/__material___crossbow_pulling_0.json | 6 + .../item/__material___crossbow_pulling_1.json | 6 + .../item/__material___crossbow_pulling_2.json | 6 + .../models/item/__material___crystal.json | 6 + .../mmdlib/models/item/__material___door.json | 6 + .../mmdlib/models/item/__material___dust.json | 6 + .../models/item/__material___endore.json | 3 + .../models/item/__material___fence.json | 3 + .../models/item/__material___fence_gate.json | 3 + .../models/item/__material___fishing_rod.json | 14 + .../item/__material___fishing_rod_cast.json | 6 + .../models/item/__material___flower_pot.json | 3 + .../mmdlib/models/item/__material___gear.json | 6 + .../mmdlib/models/item/__material___gem.json | 6 + .../models/item/__material___helmet.json | 6 + .../mmdlib/models/item/__material___hoe.json | 6 + .../models/item/__material___horse_armor.json | 6 + .../models/item/__material___ingot.json | 6 + .../models/item/__material___ladder.json | 6 + .../models/item/__material___leggings.json | 6 + .../models/item/__material___lever.json | 6 + .../item/__material___molten_bucket.json | 5 + .../models/item/__material___netherore.json | 3 + .../models/item/__material___nugget.json | 6 + .../mmdlib/models/item/__material___ore.json | 3 + .../models/item/__material___pickaxe.json | 6 + .../models/item/__material___plate.json | 8 + .../item/__material___powder_dirty.json | 6 + .../item/__material___pressure_plate.json | 3 + .../mmdlib/models/item/__material___rod.json | 6 + .../models/item/__material___scythe.json | 6 + .../models/item/__material___shard.json | 6 + .../models/item/__material___shears.json | 6 + .../models/item/__material___shield.json | 14 + .../item/__material___shield_blocking.json | 6 + .../models/item/__material___shovel.json | 6 + .../mmdlib/models/item/__material___slab.json | 3 + .../models/item/__material___smallblend.json | 6 + .../models/item/__material___smallpowder.json | 6 + .../models/item/__material___stairs.json | 3 + .../models/item/__material___sword.json | 6 + .../models/item/__material___trapdoor.json | 3 + .../item/__material___tripwire_hook.json | 6 + .../mmdlib/models/item/__material___wall.json | 3 + .../__material___weighted_pressure_plate.json | 3 + .../mmdlib_templates/provenance.json | 270 ++++++++ .../block/__material___anvil_base.png | Bin 0 -> 390 bytes .../__material___anvil_top_damaged_0.png | Bin 0 -> 290 bytes .../__material___anvil_top_damaged_1.png | Bin 0 -> 336 bytes .../__material___anvil_top_damaged_2.png | Bin 0 -> 384 bytes .../textures/block/__material___bars.png | Bin 0 -> 408 bytes .../textures/block/__material___block.png | Bin 0 -> 455 bytes .../block/__material___door_lower.png | Bin 0 -> 603 bytes .../block/__material___door_upper.png | Bin 0 -> 469 bytes .../textures/block/__material___ladder.png | Bin 0 -> 294 bytes .../block/__material___ore_overlay.png | Bin 0 -> 209 bytes .../textures/block/__material___plate.png | Bin 0 -> 455 bytes .../textures/block/__material___trapdoor.png | Bin 0 -> 366 bytes .../horse/armor/horse_armor___material__.png | Bin 0 -> 3090 bytes .../textures/item/__material___arrow.png | Bin 0 -> 197 bytes .../item/__material___arrow.untinted.png | Bin 0 -> 112 bytes .../textures/item/__material___axe.png | Bin 0 -> 158 bytes .../item/__material___axe.untinted.png | Bin 0 -> 117 bytes .../textures/item/__material___blend.png | Bin 0 -> 206 bytes .../textures/item/__material___bolt.png | Bin 0 -> 157 bytes .../textures/item/__material___boots.png | Bin 0 -> 154 bytes .../item/__material___bow_pulling_0.png | Bin 0 -> 244 bytes .../__material___bow_pulling_0.untinted.png | Bin 0 -> 92 bytes .../item/__material___bow_pulling_1.png | Bin 0 -> 244 bytes .../__material___bow_pulling_1.untinted.png | Bin 0 -> 91 bytes .../item/__material___bow_pulling_2.png | Bin 0 -> 244 bytes .../__material___bow_pulling_2.untinted.png | Bin 0 -> 95 bytes .../item/__material___bow_standby.png | Bin 0 -> 244 bytes .../__material___bow_standby.untinted.png | Bin 0 -> 84 bytes .../textures/item/__material___chestplate.png | Bin 0 -> 286 bytes .../textures/item/__material___clump.png | Bin 0 -> 367 bytes .../item/__material___crackhammer.png | Bin 0 -> 214 bytes .../__material___crackhammer.untinted.png | Bin 0 -> 113 bytes .../item/__material___crossbow_pulling_0.png | Bin 0 -> 313 bytes ...material___crossbow_pulling_0.untinted.png | Bin 0 -> 92 bytes .../item/__material___crossbow_pulling_1.png | Bin 0 -> 311 bytes ...material___crossbow_pulling_1.untinted.png | Bin 0 -> 91 bytes .../item/__material___crossbow_pulling_2.png | Bin 0 -> 305 bytes ...material___crossbow_pulling_2.untinted.png | Bin 0 -> 95 bytes .../item/__material___crossbow_standby.png | Bin 0 -> 317 bytes ...__material___crossbow_standby.untinted.png | Bin 0 -> 84 bytes .../textures/item/__material___crystal.png | Bin 0 -> 314 bytes .../textures/item/__material___door.png | Bin 0 -> 383 bytes .../textures/item/__material___dust.png | Bin 0 -> 279 bytes .../item/__material___fishing_rod_cast.png | Bin 0 -> 165 bytes ...__material___fishing_rod_cast.untinted.png | Bin 0 -> 93 bytes .../item/__material___fishing_rod_uncast.png | Bin 0 -> 165 bytes ...material___fishing_rod_uncast.untinted.png | Bin 0 -> 129 bytes .../textures/item/__material___gear.png | Bin 0 -> 305 bytes .../textures/item/__material___gem.png | Bin 0 -> 314 bytes .../textures/item/__material___helmet.png | Bin 0 -> 205 bytes .../textures/item/__material___hoe.png | Bin 0 -> 117 bytes .../item/__material___hoe.untinted.png | Bin 0 -> 109 bytes .../item/__material___horse_armor.png | Bin 0 -> 298 bytes .../textures/item/__material___ingot.png | Bin 0 -> 357 bytes .../textures/item/__material___leggings.png | Bin 0 -> 287 bytes .../textures/item/__material___nugget.png | Bin 0 -> 129 bytes .../textures/item/__material___pickaxe.png | Bin 0 -> 187 bytes .../item/__material___pickaxe.untinted.png | Bin 0 -> 109 bytes .../item/__material___powder_dirty.png | Bin 0 -> 379 bytes .../textures/item/__material___rod.png | Bin 0 -> 133 bytes .../textures/item/__material___scythe.png | Bin 0 -> 164 bytes .../item/__material___scythe.untinted.png | Bin 0 -> 104 bytes .../textures/item/__material___shard.png | Bin 0 -> 247 bytes .../textures/item/__material___shears.png | Bin 0 -> 160 bytes .../item/__material___shears.untinted.png | Bin 0 -> 134 bytes .../textures/item/__material___shovel.png | Bin 0 -> 117 bytes .../item/__material___shovel.untinted.png | Bin 0 -> 105 bytes .../textures/item/__material___smallblend.png | Bin 0 -> 148 bytes .../item/__material___smallpowder.png | Bin 0 -> 152 bytes .../textures/item/__material___sword.png | Bin 0 -> 253 bytes .../item/__material___sword.untinted.png | Bin 0 -> 99 bytes .../models/armor/__material___layer_1.png | Bin 0 -> 1126 bytes .../models/armor/__material___layer_2.png | Bin 0 -> 574 bytes .../common/builders/DefinitionLoaderTest.java | 126 ++++ .../ScriptMaterialResourcesTest.java | 247 +++++++ 254 files changed, 6675 insertions(+), 37 deletions(-) create mode 100644 docs/crafttweaker.md create mode 100644 scripts/generate_material_templates.py create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/CraftTweakerInit.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MMDLibCraftTweakerPlugin.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MaterialPacks.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MaterialTemplates.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockBuilder.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModule.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptData.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitions.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialBuilder.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___anvil.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___bars.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___block.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___bookshelf.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___button.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___door.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___double_slab.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___endore.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___fence.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___fence_gate.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___flower_pot.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___ladder.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___lever.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___molten.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___netherore.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___ore.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___plate.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___pressure_plate.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___slab.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___stairs.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___trapdoor.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___tripwire_hook.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___wall.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___weighted_pressure_plate.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil_chipped.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil_damaged.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_cap.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_cap_alt.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_post.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_post_ends.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_side.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_side_alt.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___block.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bookshelf.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button_inventory.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button_pressed.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_left.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_left_open.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_right.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_right_open.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_left.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_left_open.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_right.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_right_open.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___endore.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_open.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_wall.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_wall_open.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_inventory.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_post.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_side.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___flower_pot.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___ladder.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___lever.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___lever_on.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___netherore.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___ore.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_down.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_east.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_north.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_south.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_up.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_west.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___pressure_plate.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___pressure_plate_down.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab_double.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab_top.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs_inner.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs_outer.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_bottom.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_open.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_top.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_attached.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_attached_on.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_on.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_inventory.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_post.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_side.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_side_tall.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___weighted_pressure_plate.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___weighted_pressure_plate_down.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___anvil.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___arrow.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___axe.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bars.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___blend.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___block.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bolt.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bookshelf.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___boots.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_0.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_1.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_2.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___button.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___chestplate.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___clump.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crackhammer.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_arrow.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_firework.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_0.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_1.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_2.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crystal.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___door.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___dust.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___endore.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fence.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fence_gate.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fishing_rod.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fishing_rod_cast.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___flower_pot.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___gear.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___gem.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___helmet.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___hoe.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___horse_armor.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ingot.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ladder.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___leggings.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___lever.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___molten_bucket.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___netherore.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___nugget.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ore.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___pickaxe.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___plate.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___powder_dirty.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___pressure_plate.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___rod.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___scythe.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shard.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shears.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shield.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shield_blocking.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shovel.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___slab.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___smallblend.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___smallpowder.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___stairs.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___sword.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___trapdoor.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___tripwire_hook.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___wall.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___weighted_pressure_plate.json create mode 100644 src/main/resources/mmdlib_templates/provenance.json create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___anvil_base.png create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_0.png create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_1.png create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_2.png create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___bars.png create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___block.png create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___door_lower.png create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___door_upper.png create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___ladder.png create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___ore_overlay.png create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___plate.png create mode 100644 src/main/resources/mmdlib_templates/textures/block/__material___trapdoor.png create mode 100644 src/main/resources/mmdlib_templates/textures/entity/horse/armor/horse_armor___material__.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___arrow.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___arrow.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___axe.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___axe.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___blend.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___bolt.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___boots.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_0.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_0.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_1.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_1.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_2.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_2.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___bow_standby.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___bow_standby.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___chestplate.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___clump.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___crackhammer.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___crackhammer.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_0.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_0.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_1.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_1.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_2.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_2.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___crossbow_standby.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___crossbow_standby.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___crystal.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___door.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___dust.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_cast.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_cast.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_uncast.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_uncast.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___gear.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___gem.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___helmet.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___hoe.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___hoe.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___horse_armor.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___ingot.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___leggings.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___nugget.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___pickaxe.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___pickaxe.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___powder_dirty.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___rod.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___scythe.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___scythe.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___shard.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___shears.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___shears.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___shovel.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___shovel.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___smallblend.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___smallpowder.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___sword.png create mode 100644 src/main/resources/mmdlib_templates/textures/item/__material___sword.untinted.png create mode 100644 src/main/resources/mmdlib_templates/textures/models/armor/__material___layer_1.png create mode 100644 src/main/resources/mmdlib_templates/textures/models/armor/__material___layer_2.png create mode 100644 src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java diff --git a/build.gradle b/build.gradle index 406c15a9..59cb40dd 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ java { repositories { mavenCentral() - // JEI (optional integration; compile-time API only) + // JEI and CraftTweaker (optional integrations) maven { url = 'https://maven.blamejared.com' } maven { url = 'https://maven.theillusivec4.top/' @@ -39,6 +39,11 @@ repositories { configurations { localRuntime runtimeClasspath.extendsFrom localRuntime + // CraftTweaker's processor path brings slf4j-api 1.8 betas, whose ServiceLoader lookup finds + // Gradle's own provider from another classloader and fails; 1.7.x binds statically instead. + annotationProcessor { + resolutionStrategy.force 'org.slf4j:slf4j-api:1.7.36' + } } dependencies { @@ -51,6 +56,15 @@ dependencies { if (!providers.gradleProperty('withoutJei').isPresent()) { localRuntime "mezz.jei:jei-1.21.1-neoforge:${jei_version}" } + // Optional CraftTweaker integration: compiled against, never bundled or required. + compileOnly "com.blamejared.crafttweaker:CraftTweaker-neoforge-1.21.1:${crafttweaker_version}" + // Validates @ZenRegister/@ZenCodeType usage at compile time; needs CraftTweaker on its path too. + annotationProcessor "com.blamejared.crafttweaker:Crafttweaker_Annotation_Processors:${crafttweaker_ap_version}" + annotationProcessor "com.blamejared.crafttweaker:CraftTweaker-neoforge-1.21.1:${crafttweaker_version}" + // Dev runs include CraftTweaker; pass -PwithoutCraftTweaker to launch without it. + if (!providers.gradleProperty('withoutCraftTweaker').isPresent()) { + localRuntime "com.blamejared.crafttweaker:CraftTweaker-neoforge-1.21.1:${crafttweaker_version}" + } } neoForge { @@ -109,6 +123,7 @@ tasks.named('processResources', ProcessResources) { neoforge_version_range: neoforge_version_range, loader_version_range: loader_version_range, jei_version_range: jei_version_range, + crafttweaker_version_range: crafttweaker_version_range, curios_version_range: curios_version_range, mod_id: mod_id, mod_name: mod_name, diff --git a/docs/crafttweaker.md b/docs/crafttweaker.md new file mode 100644 index 00000000..1a64ec41 --- /dev/null +++ b/docs/crafttweaker.md @@ -0,0 +1,299 @@ +# CraftTweaker definitions + +When CraftTweaker is installed, scripts can define MMDLib materials, blocks, and block +modules. Scripts use the `mmdlib` loader and run once at startup, after every mod has +constructed and before registry events. What they build is planned, checked, and +registered like the matching `mmdlib/` files. Changing a script needs a restart; +`/reload` does not run these scripts again. + +The `mmdlib` loader inherits CraftTweaker's `crafttweaker` loader, so its types +(`IData`, item stacks, and so on) are available. Anything that needs registries or a +world, such as `` brackets or recipe managers, cannot run this early; use a +normal `crafttweaker` script for recipes. + +## Materials + +```zenscript +#loader mmdlib +import mods.mmdlib.MaterialBuilder; + +var copper = new MaterialBuilder("copper"); +copper.type = "metal"; +copper.color = 0xFFFF9B40; +copper.hardness = 3.5; +copper.strength = 8; +copper.magic = 4.5; +copper.removeItems("all").addItems("ingot", "nugget", "dust"); +copper.removeBlocks("door", "double_slab"); +copper.addHarvestTags("minecraft:needs_stone_tool"); +copper.addSpecial("blast_resistance", 8); +copper.hasFluid = false; +copper.build(); +``` + +Content is registered in MMDLib's namespace: `copper` above becomes `mmdlib:copper`, +with parts such as `mmdlib:copper_ingot`. The name can also be written +`"mmdlib:copper"`; any other namespace is rejected. A name used by a material file +or another script fails loading. + +### Properties + +| Property | Type | Default | +| --- | --- | --- | +| `name` | string, read-only | from the constructor | +| `type` | string | `generic` | +| `color` | int (ARGB) | `-1` (opaque white) | +| `hardness`, `strength`, `magic` | double, nonnegative | `0` | +| `damage` | double | `0` | +| `hasFluid` | bool | `true` | +| `fluid` | fluid module ID | `mmdlib:molten` | +| `items`, `blocks`, `harvestTags` | string[], read-only | see below | + +See [the material factory](material-factory.md) for what each stat drives. + +### Collections + +Each collection has an add and a remove function. They take any number of names, +return the builder so calls can be chained, and accept `"all"`: + +| Functions | Names | `"all"` | +| --- | --- | --- | +| `addItems` / `removeItems` | item parts, such as `ingot` or `pickaxe` | every item part | +| `addBlocks` / `removeBlocks` | block parts, such as `block` or `ore` | every block part | +| `addHarvestTags` / `removeHarvestTags` | block tag IDs | remove only: clears the tags | +| `addSpecial(key, value)` / `removeSpecial` | special stat keys | remove only: clears the stats | + +A new builder selects every item and block part, like a material file that omits +those sections; start with `removeItems("all")` to pick parts one by one. Part +names are the same as in material files, including aliases and code-registered +types. An unknown name, or a name from the other section, is an error at that call. +Projectile entities follow their items: selecting the arrow item also builds its +entity. + +### Building + +`build()` validates the values and queues the material. Call it once per +material. Changes made to the builder after `build()` are ignored. `build()` only +works during the startup run; `MaterialBuilder` is not available to other loaders. + +## Blocks + +`mods.mmdlib.BlockBuilder` builds one block with its own ID, like a +`mmdlib/standalone/block` file. The optional second argument is the block module +that decides what kind of block it is: `mmdlib:block` (a plain cube, the default), +any MMDLib block part (`mmdlib:stairs`, `mmdlib:door`, `mmdlib:pressure_plate`...), +a module another mod registers, or a script module (below). + +```zenscript +#loader mmdlib +import mods.mmdlib.BlockBuilder; + +var marble = new BlockBuilder("marble"); +marble.hardness = 1.5; +marble.sound = "stone"; +marble.build(); + +var plate = new BlockBuilder("mob_plate", "mmdlib:pressure_plate"); +plate.factoryData = {detects: "mobs"}; +plate.blockSetType = "minecraft:oak"; +plate.build(); +``` + +The block is `mmdlib:`. Its values start from the module's template, and it +is put in `minecraft:mineable/pickaxe`. + +| Property | Type | Meaning | +| --- | --- | --- | +| `name`, `module` | string, read-only | | +| `hardness`, `blastResistance` | float | mining time (-1 is unbreakable); explosion resistance | +| `friction`, `speedFactor`, `jumpFactor` | float | slipperiness (ice 0.98); walk and jump multipliers | +| `lightLevel` | int | 0 to 15 | +| `requiresCorrectTool`, `collision`, `occlusion`, `randomTicks`, `replaceable`, `hasItem` | bool | | +| `sound`, `mapColor` | string | such as `stone`, `wood`, `metal` | +| `factoryData` | map | settings for the module's Java implementation | +| `blockEntityData` | map, write-only | settings for a machine's block entity | +| `baseBlock` | block ID | what stairs copy their behavior from | +| `blockSetType` | ID | how doors, buttons and plates open: `minecraft:oak` by hand, `minecraft:iron` (default) by redstone | +| `tags`, `events` | string[], read-only | | + +Blocks, items, and modules share three collections: `addTags`/`removeTags` (block +tags here) with `removeTags("all")` to clear them, the `factoryData` map, and events. +`addEvent(event, callback)` binds a registered callback, such as +`addEvent("block-right-click", "mmdlib:open_menu")`. `removeEvents` removes bindings +by event name, and `removeEvents("all")` clears them. + +MMDLib generates the block's blockstate, models, item model, English name, loot +table, and tags from the module's template, but **not its textures**. Supply them in +`scripts/mmdlibdata/assets/mmdlib/textures/`. At startup the client log lists each +texture a block still needs. A plain block `marble` needs `block/marble.png`. Shaped +blocks use the block's name for their main texture, plus suffixed ones where the +shape needs more (doors need `block/_lower.png` and `block/_upper.png`). +A module with no MMDLib template gets nothing generated, and a warning says so; +supply its blockstate and models in `scripts/mmdlibdata` too. + +## Block modules + +`mods.mmdlib.BlockModule` defines a new kind of block, like a `data//block` +module file. It starts as a copy of the module named in the second argument +(`mmdlib:block` by default), without that module's aliases, and has the same +properties, tags, and events as a block. In addition: + +| Property | Type | Meaning | +| --- | --- | --- | +| `name` | string, read-only | `mmdlib:` | +| `factory` | ID | the Java implementation, such as `mmdlib:stairs` | +| `materialDefaults` | bool | whether a material's stats override the template's | + +`build()` registers the module immediately. Later script lines can then use it: + +```zenscript +import mods.mmdlib.BlockModule; + +var pillar = new BlockModule("pillar", "mmdlib:stairs"); +pillar.hardness = 2; +pillar.build(); + +var steps = new BlockBuilder("marble_steps", "mmdlib:pillar"); +steps.build(); + +var tin = new MaterialBuilder("tin"); +tin.addBlocks("pillar"); // tin_pillar +tin.build(); +``` + +Material files can select script modules too: scripts run before material files are +read. A material's part gets generated art from the template of the module it was copied +from (or else its factory's): `tin_pillar` looks like stairs, in the material color. + +## Items + +`mods.mmdlib.ItemBuilder` builds one item with its own ID, like a +`mmdlib/standalone/item` file. The optional second argument is the item module: +`mmdlib:ingot` (a plain item, the default), any MMDLib item part (`mmdlib:sword`, +`mmdlib:bow`, `mmdlib:helmet`...), a module another mod registers, or a script item +module. + +```zenscript +#loader mmdlib +import mods.mmdlib.ItemBuilder; + +var wrench = new ItemBuilder("wrench"); +wrench.durability = 250; +wrench.addTags("c:tools/wrench"); +wrench.build(); + +var longbow = new ItemBuilder("longbow", "mmdlib:bow"); +longbow.durability = 600; +longbow.build(); +``` + +| Property | Type | Meaning | +| --- | --- | --- | +| `name`, `module` | string, read-only | | +| `stackSize` | int | 1 to 99 | +| `durability` | int | uses before breaking; 0 for none. Setting it also sets `stackSize` to 1 | +| `fireResistant` | bool | survives fire and lava | +| `container` | item ID | left in the crafting grid after use, such as `minecraft:bucket`; `""` for nothing | +| `factoryData` | map | settings for the module's Java implementation | +| `tags`, `events` | string[], read-only | | + +Tags are item tags. `addTags`/`removeTags` and `addEvent`/`removeEvents` work as +they do for blocks. An item has no tags unless its script module gave it some. + +MMDLib generates the item's model, English name, and tags, but not its textures. +The client log lists what each item needs: a plain item `wrench` needs +`item/wrench.png`, and a bow `longbow` needs `item/longbow_standby.png` and +`item/longbow_pulling_0.png` to `_2.png`. + +## Item modules + +`mods.mmdlib.ItemModule` defines a new kind of item. It starts as a copy of the +module named in the second argument (`mmdlib:ingot` by default), without that +module's aliases, and has the same properties as an item, plus a settable `factory`. +`build()` registers it at once. Materials then select it by name, in scripts and in +material files: + +```zenscript +import mods.mmdlib.ItemModule; + +var widget = new ItemModule("widget"); +widget.stackSize = 16; +widget.addTags("c:widgets"); +widget.build(); + +var tin = new MaterialBuilder("tin"); +tin.addItems("widget"); // tin_widget, in c:widgets, drawn like a tin ingot +tin.build(); +``` + +A module's tags go to every material part built from it. The part's art comes from +the template of the module it was copied from, in the material color. + +## Generated resources + +Script materials need no resource pack, and script blocks and items need only their +textures. +MMDLib adds two packs whenever CraftTweaker is installed. Both are always enabled, for +client resources and for world data: + +1. `mmdlib/script_materials` is generated at startup from the materials, blocks, and + items the scripts built. For materials it contains: + * blockstates and item and block models for every selected part; + * textures: MMDLib's blank templates multiplied by the material color; + * English names, such as "Copper Ingot" and "Block of Copper"; + * block loot tables; + * block tags: the module's tags, `minecraft:mineable/pickaxe`, the harvest tags, + and a `needs_*_tool` tag from hardness (5 stone, 8 iron, 10 diamond); + * item tags: tool, armor and enchantability tags, `minecraft:arrows`, and + `c:ingots/`, `c:dusts/` and the like; + * the fluid's `c:molten_` tag. +2. `mmdlib/mmdlibdata` is the `scripts/mmdlibdata` folder, if it exists. It is a + normal pack root (`assets/`, `data/`, an optional `pack.mcmeta`) placed above the + generated pack. + +Recipes are not generated; add them from scripts. Parts from other mods' +code-registered types get no generated assets; their mod supplies them. + +### Replacing artwork + +* **One material's texture**: put finished art at the texture's own path, such as + `scripts/mmdlibdata/assets/mmdlib/textures/item/copper_ingot.png`. It is used as + drawn, with no tint. Models, blockstates, loot tables, tags, and names can be + replaced the same way. Lang files merge with the generated names, so a lang file + only needs the keys it changes. +* **A blank for every script material**: put a file at the template's path under + `scripts/mmdlibdata/templates/`, mirroring MMDLib's `mmdlib_templates/`. For example, + `templates/textures/item/__material___ingot.png` replaces the ingot blank. + `__material__` stands for the material's name. + * A texture template is a grayscale image whose white is the material color. + * An optional `.untinted.png` beside it is drawn over the result unchanged; + MMDLib uses this for handles, strings, and feathers. + * Model and blockstate templates are JSON in which `__material__` is replaced. + +The generated textures are colored already, so script materials set the runtime +`tinted` flag to false and the client does not tint them a second time. A JSON +material can set `"runtime": {"tinted": false}` for the same reason. + +MMDLib's templates come from Base Metals artwork and vanilla model structure. See +`scripts/generate_material_templates.py`; `provenance.json` beside the templates +records each source sprite. + +Clients need the same scripts and `scripts/mmdlibdata` folder as the server, since +items, blocks, and their resources are created at startup on both sides. + +## Not covered yet + +* Material events, runtime settings (sound, map color, block set type), and parent + materials. These need the material file format for now. +* Behavior written in script: event handlers as script functions. This is next. +* Item template fields that are kept but not applied to registered items yet: lore, + food, use time and animation, attribute modifiers, and tool actions. Scripts do not + expose them until they work. + +## Implementation + +The integration lives in `zone.moddev.mc.lib.compat.CraftTweaker`. CraftTweaker +finds its plugin and script classes by annotation. The only reference from the rest +of MMDLib is the `CraftTweakerInit.register(modBus)` call in MMDLib's constructor, made +only when CraftTweaker is loaded. That call adds a code material source to +`MaterialExtensions.GLOBAL`; see [startup definition loading](definition-loading.md#stages). diff --git a/docs/definition-loading.md b/docs/definition-loading.md index 449e56eb..08cadc19 100644 --- a/docs/definition-loading.md +++ b/docs/definition-loading.md @@ -100,12 +100,19 @@ registered when that mod queues them on its own `RuntimeContentRegistrar` 2. After that phase completes, discover/decode item, block, and fluid modules in parallel across mod files. Collect material paths without decoding them. 3. Merge results in a defined order and install the modules, rejecting duplicate IDs. -4. Decode materials against the completed module/callback catalog, bind their IDs, - build content plans, and check generated registry IDs for collisions. Every - planned item and block definition's `events` bindings, custom types included, - are validated against the callback catalog; an unknown event or callback fails - loading with the material file and definition named. -5. Retain results, index their item and block definitions (and owning materials) +4. Call each definition source registered with + `MaterialExtensions.registerDefinitionSource`, such as MMDLib's + [CraftTweaker scripts](crafttweaker.md), once. A source may register item and + block modules and callbacks; everything decoded below can use them. It returns + materials, standalone blocks and standalone items. +5. Decode materials against the completed module/callback catalog, bind their IDs, + build content plans, and check generated registry IDs for collisions. Then plan + the sources' materials with the same checks. Every planned item and block + definition's `events` bindings, custom types included, are validated against the + callback catalog; an unknown event or callback fails loading with the material + file (or source) and definition named. A source material, standalone block or item + whose ID matches a file's, or another source's, fails loading. +6. Retain results, index their item and block definitions (and owning materials) for the runtime event bridge, queue MMDLib's playable example content, and publish definitions during NeoForge registry events. diff --git a/gradle.properties b/gradle.properties index 69c3e023..0c894216 100644 --- a/gradle.properties +++ b/gradle.properties @@ -31,6 +31,11 @@ parchment_mappings_version = 2024.11.17 jei_version = 19.57.0.451 jei_version_range = [19,) +# Optional CraftTweaker integration (compile-time; dev runtime only) +crafttweaker_version = 21.0.39 +crafttweaker_version_range = [21.0,) +crafttweaker_ap_version = 4.0.0.21 + # Optional equipment API; backpack mods are companion runtime fixtures only. curios_version = 9.5.1+1.21.1 curios_version_range = [9.5.1,10) diff --git a/scripts/generate_material_templates.py b/scripts/generate_material_templates.py new file mode 100644 index 00000000..cf3ef542 --- /dev/null +++ b/scripts/generate_material_templates.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +"""Build MMDLib's material templates: blank textures, models and blockstates for generated +materials. Every template name contains __material__, which the runtime replaces with a material. + +--source regenerates the textures under mmdlib_templates/textures, each +as two PNGs: + + .png the material's pixels as grayscale, brightest pixel white; the runtime + multiplies it by the material color + .untinted.png everything else (handles, strings, white highlights), kept as drawn; + omitted when the sprite has no such pixels + +A pixel stays untinted when it is clearly not material: wood or leather brown, or bright neutral +string, feathers and highlights. Sources come from a fixed list of strongly colored Base Metals +materials (blue and red), so their metal never reads as brown or neutral. +Re-running the script is deterministic; provenance.json records each source and its hash. + +--minecraft regenerates mmdlib_templates/assets: blockstates and models. Block +forms copy the state structure of an analog vanilla block (iron door for doors, oak fence for +fences...) and use the vanilla models as parents, overriding only the textures that should be +material. The vanilla files are read, never copied into the output. + + python3 scripts/generate_material_templates.py [--source DIR] [--minecraft JAR] [--preview out.png] +""" +import argparse, colorsys, hashlib, json, struct, zipfile, zlib +from pathlib import Path + +# Materials whose hue is far from wood brown and string white, best first. +SOURCES = ['aquarium', 'starsteel', 'adamantine'] +TOKEN = '__material__' + +# template path (relative to textures/, with TOKEN) -> source path pattern (relative to the +# Base Metals textures dir; {m} is the material). Several patterns are tried in order. +ITEMS = { + 'ingot': ['item/{m}_ingot'], 'nugget': ['item/{m}_nugget'], 'dust': ['item/{m}_powder'], + 'smallpowder': ['item/{m}_smallpowder'], 'gear': ['item/{m}_gear'], 'rod': ['item/{m}_rod'], + 'blend': ['item/{m}_blend', 'item/{m}_powder'], 'smallblend': ['item/{m}_smallblend', 'item/{m}_smallpowder'], + 'clump': ['item/{m}_clump'], 'crystal': ['item/{m}_crystal'], 'shard': ['item/{m}_shard'], + 'powder_dirty': ['item/{m}_powder_dirty'], 'gem': ['item/{m}_crystal'], + 'crackhammer': ['item/{m}_crackhammer'], 'scythe': ['item/{m}_scythe'], 'axe': ['item/{m}_axe'], + 'hoe': ['item/{m}_hoe'], 'pickaxe': ['item/{m}_pickaxe'], 'shovel': ['item/{m}_shovel'], + 'sword': ['item/{m}_sword'], 'shears': ['item/{m}_shears'], 'helmet': ['item/{m}_helmet'], + 'chestplate': ['item/{m}_chestplate'], 'leggings': ['item/{m}_leggings'], 'boots': ['item/{m}_boots'], + 'horse_armor': ['item/{m}_horse_armor'], 'arrow': ['item/{m}_arrow'], 'bolt': ['item/{m}_bolt'], + 'bow_standby': ['item/{m}_bow_standby'], 'bow_pulling_0': ['item/{m}_bow_pulling_0'], + 'bow_pulling_1': ['item/{m}_bow_pulling_1'], 'bow_pulling_2': ['item/{m}_bow_pulling_2'], + 'crossbow_standby': ['item/{m}_crossbow_standby'], 'crossbow_pulling_0': ['item/{m}_crossbow_pulling_0'], + 'crossbow_pulling_1': ['item/{m}_crossbow_pulling_1'], 'crossbow_pulling_2': ['item/{m}_crossbow_pulling_2'], + 'fishing_rod_cast': ['item/{m}_fishing_rod_cast'], 'fishing_rod_uncast': ['item/{m}_fishing_rod_uncast'], +} +BLOCKS = { + 'block': ['block/{m}_block'], 'plate': ['block/{m}_plate', 'block/{m}_block'], + 'bars': ['block/{m}_bars'], 'door_lower': ['block/{m}_door_lower'], 'door_upper': ['block/{m}_door_upper'], + 'trapdoor': ['block/{m}_trapdoor'], 'ore_overlay': ['block/ore_overlays/antimony_ore'], + 'anvil_base': ['block/{m}_anvil_base'], 'anvil_top_damaged_0': ['block/{m}_anvil_top_damaged_0'], + 'anvil_top_damaged_1': ['block/{m}_anvil_top_damaged_1'], 'anvil_top_damaged_2': ['block/{m}_anvil_top_damaged_2'], +} +OTHER = { + 'models/armor/' + TOKEN + '_layer_1': ['models/armor/{m}_layer_1'], + 'models/armor/' + TOKEN + '_layer_2': ['models/armor/{m}_layer_2'], + 'entity/horse/armor/horse_armor_' + TOKEN: ['entity/horse/armor/horse_armor_{m}'], +} +# Sprites that are all material: no untinted layer, whatever the pixel test says. +WHOLE = {'block', 'plate', 'ore_overlay', 'ingot', 'nugget', 'dust', 'smallpowder', 'blend', + 'smallblend', 'clump', 'crystal', 'shard', 'powder_dirty', 'gem', 'gear', 'rod', 'bars', + 'anvil_base', 'anvil_top_damaged_0', 'anvil_top_damaged_1', 'anvil_top_damaged_2'} + + +def read_png(path): + data = path.read_bytes() + if data[:8] != b'\x89PNG\r\n\x1a\n': raise ValueError(f'{path}: not a PNG') + pos, idat, palette, trns = 8, b'', None, None + while pos < len(data): + length, kind = struct.unpack('>I4s', data[pos:pos + 8]); body = data[pos + 8:pos + 8 + length] + if kind == b'IHDR': width, height, depth, ctype, _, _, interlace = struct.unpack('>IIBBBBB', body) + elif kind == b'PLTE': palette = [tuple(body[i:i + 3]) for i in range(0, len(body), 3)] + elif kind == b'tRNS': trns = body + elif kind == b'IDAT': idat += body + pos += 12 + length + if interlace: raise ValueError(f'{path}: interlaced PNGs are not supported') + channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[ctype] + bpp = max(1, channels * depth // 8); stride = (width * channels * depth + 7) // 8 + raw = zlib.decompress(idat); rows, prev, at = [], bytearray(stride), 0 + for _ in range(height): + ftype = raw[at]; line = bytearray(raw[at + 1:at + 1 + stride]); at += 1 + stride + for i in range(stride): + a = line[i - bpp] if i >= bpp else 0; b = prev[i]; c = prev[i - bpp] if i >= bpp else 0 + if ftype == 1: line[i] = (line[i] + a) & 255 + elif ftype == 2: line[i] = (line[i] + b) & 255 + elif ftype == 3: line[i] = (line[i] + (a + b) // 2) & 255 + elif ftype == 4: + p = a + b - c; pa, pb, pc = abs(p - a), abs(p - b), abs(p - c) + line[i] = (line[i] + (a if pa <= pb and pa <= pc else b if pb <= pc else c)) & 255 + rows.append(line); prev = line + pixels = [] + for line in rows: + if depth < 8: + per = 8 // depth; mask = (1 << depth) - 1 + values = [(line[i // per] >> (8 - depth * (i % per + 1))) & mask for i in range(width)] + else: + values = list(line) + for x in range(width): + if ctype == 3: + index = values[x]; r, g, b = palette[index] + a = trns[index] if trns is not None and index < len(trns) else 255 + elif ctype == 6: r, g, b, a = values[x * 4:x * 4 + 4] + elif ctype == 2: r, g, b = values[x * 3:x * 3 + 3]; a = 255 + elif ctype == 4: r = g = b = values[x * 2]; a = values[x * 2 + 1] + else: + r = g = b = values[x] * (255 // ((1 << depth) - 1)) if depth < 8 else values[x]; a = 255 + pixels.append((r, g, b, a)) + return width, height, pixels + + +def write_png(path, width, height, pixels): + raw = b''.join(b'\x00' + bytes(v for p in pixels[y * width:(y + 1) * width] for v in p) for y in range(height)) + def chunk(kind, body): return struct.pack('>I', len(body)) + kind + body + struct.pack('>I', zlib.crc32(kind + body) & 0xffffffff) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0)) + + chunk(b'IDAT', zlib.compress(raw, 9)) + chunk(b'IEND', b'')) + + +def split(pixels, whole): + """Splits a sprite into (tint, untinted) pixel lists; tint is normalized grayscale.""" + tint, keep, lums = [], [], [] + for r, g, b, a in pixels: + h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255) + # Only clearly foreign pixels stay as drawn: wood and leather browns, and bright neutral + # string, feathers and highlights. Every other pixel, accents included, is material. + brown = 0.02 <= h <= 0.17 and s > 0.25 and v > 0.15 + bright = s < 0.18 and v > 0.7 + material = whole or (a > 0 and not (brown or bright)) + if a == 0: tint.append(None); keep.append((0, 0, 0, 0)) + elif material: + lum = 0.299 * r + 0.587 * g + 0.114 * b; lums.append(lum); tint.append((lum, a)); keep.append((0, 0, 0, 0)) + else: tint.append(None); keep.append((r, g, b, a)) + # Stretch so the brightest material pixel (98th percentile, ignoring stray sparkles) is white. + top = sorted(lums)[int(len(lums) * 0.98)] if lums else 255 + scale = 255 / max(top, 1) + out = [(0, 0, 0, 0) if t is None else (min(255, round(t[0] * scale)),) * 3 + (t[1],) for t in tint] + return out, keep if any(p[3] for p in keep) else None + + +def main(): + repo = Path(__file__).resolve().parents[1] + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--source', type=Path, help='Base Metals assets dir (contains textures/)') + parser.add_argument('--minecraft', type=Path, help='Minecraft client jar, for vanilla blockstates and models') + parser.add_argument('--output', type=Path, default=repo / 'src/main/resources/mmdlib_templates') + parser.add_argument('--preview', type=Path, help='Also write a contact sheet tinted with sample colors') + args = parser.parse_args() + if not args.source and not args.minecraft: parser.error('give --source, --minecraft or both') + if args.source: textures_from(args.source, args.output, args.preview) + if args.minecraft: models_from(args.minecraft, args.output) + + +def textures_from(source, output, preview_path): + textures = source / 'textures'; out = output / 'textures' + targets = {('item/' + TOKEN + '_' + k): v for k, v in ITEMS.items()} + targets.update({('block/' + TOKEN + '_' + k): v for k, v in BLOCKS.items()}); targets.update(OTHER) + provenance, previews = {}, [] + for target, patterns in sorted(targets.items()): + for pattern in patterns: + found = next(((m, textures / (pattern.format(m=m) + '.png')) for m in SOURCES + if (textures / (pattern.format(m=m) + '.png')).is_file()), None) + if found: break + if not found: raise SystemExit(f'No source sprite for {target}: tried {patterns} for {SOURCES}') + material, file = found + width, height, pixels = read_png(file) + form = target.rsplit('/', 1)[-1].replace(TOKEN + '_', '') + tint, keep = split(pixels, form in WHOLE) + save(out, target, width, height, tint, keep) + mcmeta = file.with_name(file.name + '.mcmeta') + if mcmeta.is_file(): (out / (target + '.png.mcmeta')).write_bytes(mcmeta.read_bytes()) + provenance[target] = {'source': file.relative_to(source).as_posix(), + 'sha256': hashlib.sha256(file.read_bytes()).hexdigest(), 'untinted': bool(keep)} + previews.append((width, height, tint, keep)) + for target, (width, height, tint, keep, note) in derived(out).items(): + save(out, target, width, height, tint, keep) + provenance[target] = {'derived': note, 'untinted': bool(keep)} + previews.append((width, height, tint, keep)) + (output / 'provenance.json').write_text(json.dumps(provenance, indent=2, sort_keys=True) + '\n') + if preview_path: preview(preview_path, previews) + print(f'Wrote {len(provenance)} texture templates to {out}') + + +def save(out, target, width, height, tint, keep): + write_png(out / (target + '.png'), width, height, tint) + untinted = out / (target + '.untinted.png') + if keep: write_png(untinted, width, height, keep) + elif untinted.exists(): untinted.unlink() + + +def load(out, target): + """A written template as (width, height, tint, keep).""" + w, h, tint = read_png(out / (target + '.png')) + file = out / (target + '.untinted.png') + return w, h, tint, read_png(file)[2] if file.is_file() else [(0, 0, 0, 0)] * (w * h) + + +def derived(out): + """Templates Base Metals has no sprite for, built from templates written above.""" + result = {} + # Ladder: two rails and four rungs cut from the block texture, with a dark edge below each. + w, h, block, _ = load(out, 'block/' + TOKEN + '_block') + def solid(x, y): return x in (2, 3, 12, 13) or (y % 4 == 1 and 2 <= x <= 13) + tint = [] + for y in range(16): + for x in range(16): + if solid(x, y): tint.append(block[y * w + x]) + elif solid(x, y - 1): tint.append((40, 40, 40, 255)) + else: tint.append((0, 0, 0, 0)) + result['block/' + TOKEN + '_ladder'] = (16, 16, tint, None, 'rails and rungs cut from block') + # Door item: both door halves stacked, then halved to 8x16 and centered. + _, _, lt, lk = load(out, 'block/' + TOKEN + '_door_lower') + _, _, ut, uk = load(out, 'block/' + TOKEN + '_door_upper') + tall_t, tall_k = ut + lt, uk + lk + tint, keep = [], [] + for y in range(16): + for x in range(16): + if 4 <= x < 12: + sx, sy = (x - 4) * 2, y * 2 + tint.append(tall_t[sy * 16 + sx]); keep.append(tall_k[sy * 16 + sx]) + else: + tint.append((0, 0, 0, 0)); keep.append((0, 0, 0, 0)) + result['item/' + TOKEN + '_door'] = (16, 16, tint, keep if any(p[3] for p in keep) else None, + 'door halves stacked and halved') + return result + + +T = 'mmdlib:block/' + TOKEN +I = 'mmdlib:item/' + TOKEN + +# Part -> the vanilla block whose states and models it copies. +ANALOGS = { + 'block': 'iron_block', 'slab': 'cut_copper_slab', 'stairs': 'cut_copper_stairs', + 'wall': 'cobblestone_wall', 'fence': 'oak_fence', 'fence_gate': 'oak_fence_gate', + 'door': 'iron_door', 'trapdoor': 'iron_trapdoor', 'button': 'stone_button', + 'pressure_plate': 'stone_pressure_plate', 'weighted_pressure_plate': 'heavy_weighted_pressure_plate', + 'bars': 'iron_bars', 'ladder': 'ladder', 'lever': 'lever', 'tripwire_hook': 'tripwire_hook', + 'flower_pot': 'flower_pot', 'bookshelf': 'bookshelf', 'anvil': 'anvil', +} +# Vanilla models named outside their block's prefix. +RENAMED = {'cut_copper': T + '_slab_double', 'chipped_anvil': T + '_anvil_chipped', + 'damaged_anvil': T + '_anvil_damaged'} +# Vanilla textures that become material textures; all others (lever handle, dirt, hook, books) +# stay vanilla. +MATERIAL_TEXTURES = { + 'block/iron_block': T + '_block', 'block/cut_copper': T + '_block', 'block/cobblestone': T + '_block', + 'block/oak_planks': T + '_block', 'block/stone': T + '_block', 'block/flower_pot': T + '_block', + 'block/iron_door_bottom': T + '_door_lower', 'block/iron_door_top': T + '_door_upper', + 'block/iron_trapdoor': T + '_trapdoor', 'block/iron_bars': T + '_bars', 'block/ladder': T + '_ladder', + 'block/anvil': T + '_anvil_base', 'block/anvil_top': T + '_anvil_top_damaged_0', + 'block/chipped_anvil_top': T + '_anvil_top_damaged_1', 'block/damaged_anvil_top': T + '_anvil_top_damaged_2', + 'item/iron_door': I + '_door', +} +CUTOUT = {'door', 'trapdoor', 'bars', 'ladder', 'lever', 'tripwire_hook', 'flower_pot'} +GENERATED = ['ingot', 'nugget', 'dust', 'smallpowder', 'gear', 'rod', 'blend', 'smallblend', 'clump', + 'crystal', 'shard', 'powder_dirty', 'gem', 'shears', 'helmet', 'chestplate', 'leggings', + 'boots', 'horse_armor', 'arrow', 'bolt'] +HANDHELD = ['axe', 'hoe', 'pickaxe', 'shovel', 'sword', 'crackhammer', 'scythe'] +PLATE_FACES = {'down': ([0, 0, 0], [16, 1, 16]), 'up': ([0, 15, 0], [16, 16, 16]), + 'north': ([0, 0, 0], [16, 16, 1]), 'south': ([0, 0, 15], [16, 16, 16]), + 'west': ([0, 0, 0], [1, 16, 16]), 'east': ([15, 0, 0], [16, 16, 16])} + + +def ref(value): + """A vanilla resource reference with its namespace and without it: ('minecraft', 'block/x').""" + return value.split(':', 1) if ':' in value else ('minecraft', value) + + +def models_from(jar, output): + vanilla = zipfile.ZipFile(jar) + def read(path): return json.loads(vanilla.read('assets/minecraft/' + path + '.json')) + assets = output / 'assets/mmdlib' + for old in list(assets.rglob('*.json')) if assets.is_dir() else []: old.unlink() + files = {} + def put(path, data): files[path] = data + def our_model(vanilla_ref, part, analog): + namespace, path = ref(vanilla_ref) + name = path.removeprefix('block/') + if name in RENAMED: ours = RENAMED[name] + elif name.startswith(analog): ours = T + '_' + part + name[len(analog):] + else: raise SystemExit(f'{analog}: unexpected model {vanilla_ref}') + define(name, ours, part) + return ours + def define(name, ours, part): + path = 'models/block/' + ours.split('/', 1)[1] + if path in files: return + textures, model, chain = {}, read('models/block/' + name), [] + while model is not None: + chain.append(model) + parent = model.get('parent') + model = read('models/' + ref(parent)[1]) if parent and ref(parent)[1] != 'block/block' and not ref(parent)[1].startswith('builtin') else None + for m in reversed(chain): textures.update(m.get('textures', {})) + overrides = {k: MATERIAL_TEXTURES[ref(v)[1]] for k, v in textures.items() + if not v.startswith('#') and ref(v)[1] in MATERIAL_TEXTURES} + data = {'parent': 'minecraft:block/' + name, 'textures': overrides} + if part in CUTOUT: data['render_type'] = 'minecraft:cutout' + put(path, data) + for part, analog in ANALOGS.items(): + state = read('blockstates/' + analog) + def rewrite(node): + if isinstance(node, dict): + return {k: (our_model(v, part, analog) if k == 'model' else rewrite(v)) for k, v in node.items()} + if isinstance(node, list): return [rewrite(v) for v in node] + return node + state = rewrite(state) + if part == 'anvil': + # MMDLib's anvil keeps its wear in a damage property instead of three blocks. + variants = {} + for key, variant in state['variants'].items(): + for damage, suffix in enumerate(['', '_chipped', '_damaged']): + our_model('minecraft:block/' + ['anvil', 'chipped_anvil', 'damaged_anvil'][damage], part, analog) + variants[f'damage={damage},{key}'] = dict(variant, model=T + '_anvil' + suffix) + state = {'variants': variants} + put('blockstates/' + TOKEN + '_' + part, state) + item = read('models/item/' + analog) + if part == 'flower_pot': item = {'parent': T + '_flower_pot'} + elif 'layer0' in item.get('textures', {}): + layer = ref(item['textures']['layer0'])[1] + item = {'parent': 'minecraft:item/generated', + 'textures': {'layer0': MATERIAL_TEXTURES.get(layer, 'minecraft:' + layer)}} + else: + item = {'parent': our_model(item['parent'], part, analog)} + put('models/item/' + TOKEN + '_' + part, item) + # The double slab is a full block in every state. + put('blockstates/' + TOKEN + '_double_slab', {'variants': {'': {'model': T + '_slab_double'}}}) + # Plates: one face-hugging slab per direction, MMDLib's own shape. + put('blockstates/' + TOKEN + '_plate', + {'variants': {f'facing={d}': {'model': T + '_plate_' + d} for d in PLATE_FACES}}) + for d, (low, high) in PLATE_FACES.items(): + put('models/block/' + TOKEN + '_plate_' + d, + {'parent': 'minecraft:block/block', 'textures': {'all': T + '_plate', 'particle': T + '_plate'}, + 'elements': [{'from': low, 'to': high, 'faces': {f: {'texture': '#all'} for f in + ['up', 'down', 'north', 'south', 'east', 'west']}}]}) + put('models/item/' + TOKEN + '_plate', + {'parent': 'mmdlib:block/plate', 'textures': {k: T + '_plate' for k in ['top', 'side', 'bottom']}}) + # Ores: the vanilla base stone with the tinted overlay drawn over it. + for part, base in [('ore', 'stone'), ('netherore', 'netherrack'), ('endore', 'end_stone')]: + faces = lambda texture: {f: {'texture': texture, 'cullface': f} for f in + ['up', 'down', 'north', 'south', 'east', 'west']} + put('blockstates/' + TOKEN + '_' + part, {'variants': {'': {'model': T + '_' + part}}}) + put('models/block/' + TOKEN + '_' + part, + {'parent': 'minecraft:block/block', 'render_type': 'minecraft:cutout', + 'textures': {'particle': 'minecraft:block/' + base, 'base': 'minecraft:block/' + base, + 'overlay': T + '_ore_overlay'}, + 'elements': [{'from': [0, 0, 0], 'to': [16, 16, 16], 'faces': faces('#base')}, + {'from': [0, 0, 0], 'to': [16, 16, 16], 'faces': faces('#overlay')}]}) + put('models/item/' + TOKEN + '_' + part, {'parent': T + '_' + part}) + # The molten fluid: rendered by its fluid type, so the block itself has no model. + put('blockstates/' + TOKEN + '_molten', {'variants': {'': {'model': 'minecraft:block/air'}}}) + put('models/item/' + TOKEN + '_molten_bucket', + {'parent': 'neoforge:item/bucket', 'loader': 'neoforge:fluid_container', 'fluid': 'mmdlib:' + TOKEN + '_molten'}) + # Items. + for part in GENERATED: + put('models/item/' + TOKEN + '_' + part, {'parent': 'minecraft:item/generated', 'textures': {'layer0': I + '_' + part}}) + for part in HANDHELD: + put('models/item/' + TOKEN + '_' + part, {'parent': 'minecraft:item/handheld', 'textures': {'layer0': I + '_' + part}}) + put('models/item/' + TOKEN + '_bow', {'parent': 'minecraft:item/bow', 'textures': {'layer0': I + '_bow_standby'}, + 'overrides': [{'predicate': {'pulling': 1}, 'model': I + '_bow_pulling_0'}, + {'predicate': {'pulling': 1, 'pull': 0.65}, 'model': I + '_bow_pulling_1'}, + {'predicate': {'pulling': 1, 'pull': 0.9}, 'model': I + '_bow_pulling_2'}]}) + for n in range(3): + put(f'models/item/{TOKEN}_bow_pulling_{n}', {'parent': I + '_bow', 'textures': {'layer0': f'{I}_bow_pulling_{n}'}}) + put('models/item/' + TOKEN + '_crossbow', {'parent': 'minecraft:item/crossbow', 'textures': {'layer0': I + '_crossbow_standby'}, + 'overrides': [{'predicate': {'pulling': 1}, 'model': I + '_crossbow_pulling_0'}, + {'predicate': {'pulling': 1, 'pull': 0.65}, 'model': I + '_crossbow_pulling_1'}, + {'predicate': {'pulling': 1, 'pull': 0.9}, 'model': I + '_crossbow_pulling_2'}, + {'predicate': {'charged': 1}, 'model': I + '_crossbow_arrow'}, + {'predicate': {'charged': 1, 'firework': 1}, 'model': I + '_crossbow_firework'}]}) + for n in range(3): + put(f'models/item/{TOKEN}_crossbow_pulling_{n}', {'parent': 'minecraft:item/crossbow', 'textures': {'layer0': f'{I}_crossbow_pulling_{n}'}}) + for loaded in ['arrow', 'firework']: + # Base Metals draws one fully loaded sprite; both loaded states use it. + put(f'models/item/{TOKEN}_crossbow_{loaded}', {'parent': 'minecraft:item/crossbow', 'textures': {'layer0': I + '_crossbow_pulling_2'}}) + put('models/item/' + TOKEN + '_fishing_rod', {'parent': 'minecraft:item/handheld_rod', 'textures': {'layer0': I + '_fishing_rod_uncast'}, + 'overrides': [{'predicate': {'cast': 1}, 'model': I + '_fishing_rod_cast'}]}) + put('models/item/' + TOKEN + '_fishing_rod_cast', {'parent': 'minecraft:item/fishing_rod', 'textures': {'layer0': I + '_fishing_rod_cast'}}) + put('models/item/' + TOKEN + '_shield', {'parent': 'mmdlib:item/shield', 'textures': {'shield': T + '_block'}, + 'overrides': [{'predicate': {'blocking': 1}, 'model': I + '_shield_blocking'}]}) + put('models/item/' + TOKEN + '_shield_blocking', {'parent': 'mmdlib:item/shield_blocking', 'textures': {'shield': T + '_block'}}) + for path, data in sorted(files.items()): + target = assets / (path + '.json'); target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(json.dumps(data, indent=2) + '\n') + print(f'Wrote {len(files)} model and blockstate templates to {assets}') + + +def preview(path, sprites, colors=(0xFF3A6FE0, 0xFFE0A030, 0xFF40C060, 0xFFB0B0B8), scale=4): + """A sheet: one column per sprite, one row per sample color, each 16x16 cell upscaled.""" + cell = 16 * scale + 4; width = cell * len(sprites); height = cell * len(colors) + canvas = [(40, 40, 40, 255)] * (width * height) + for col, (w, h, tint, keep) in enumerate(sprites): + for row, color in enumerate(colors): + cr, cg, cb = (color >> 16) & 255, (color >> 8) & 255, color & 255 + for y in range(min(h, 16)): + for x in range(min(w, 16)): + p = tint[y * w + x]; k = keep[y * w + x] if keep else (0, 0, 0, 0) + if k[3]: c = k + elif p[3]: c = (p[0] * cr // 255, p[1] * cg // 255, p[2] * cb // 255, 255) + else: continue + for dy in range(scale): + for dx in range(scale): + canvas[(row * cell + y * scale + dy) * width + col * cell + x * scale + dx] = c + write_png(path, width, height, canvas) + + +if __name__ == '__main__': + main() diff --git a/src/main/java/zone/moddev/mc/lib/MMDLib.java b/src/main/java/zone/moddev/mc/lib/MMDLib.java index 3f475b04..0f5c9cf7 100644 --- a/src/main/java/zone/moddev/mc/lib/MMDLib.java +++ b/src/main/java/zone/moddev/mc/lib/MMDLib.java @@ -32,6 +32,7 @@ import zone.moddev.mc.lib.common.types.MMDBlock; import zone.moddev.mc.lib.common.types.MMDItem; import zone.moddev.mc.lib.common.types.MMDMaterial; +import zone.moddev.mc.lib.compat.CraftTweaker.CraftTweakerInit; /** * MMDLib's mod entry point. At mod construction it loads every mod's {@code mmdlib/} definitions @@ -116,6 +117,9 @@ public MMDLib(IEventBus modBus, ModContainer container) { NeoForge.EVENT_BUS.addListener( zone.moddev.mc.lib.machine.neoforge.MachinePartsLoader::register); modBus.addListener(zone.moddev.mc.lib.util.Registries::register); + + if (ModList.get().isLoaded("crafttweaker")) CraftTweakerInit.register(modBus); + modBus.addListener(this::publishDefinitions); modBus.addListener((FMLConstructModEvent event) -> event.enqueueWork(this::loadDefinitions)); new ItemEventBridge( diff --git a/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java b/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java index ffbf26f5..b2f4a3e2 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java +++ b/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java @@ -40,6 +40,62 @@ public record ItemType(ItemTypeFactory factory, Optional requiredMod) */ public record BlockType(BlockTypeFactory factory, Optional requiredMod) {} + /** + * Definitions built in code rather than read from {@code mmdlib/} files. + * + * @param materials named materials, planned like {@code mmdlib/material} files + * @param standaloneBlocks standalone blocks by registry ID, planned like {@code + * mmdlib/standalone/block} files + * @param standaloneItems standalone items by registry ID, planned like {@code + * mmdlib/standalone/item} files + */ + public record Definitions( + java.util.Collection materials, + Map standaloneBlocks, + Map standaloneItems) { + /** Nothing. */ + public static final Definitions EMPTY = new Definitions(java.util.List.of(), Map.of(), Map.of()); + + /** Copies every collection. */ + public Definitions { + materials = java.util.List.copyOf(materials); + standaloneBlocks = Map.copyOf(standaloneBlocks); + standaloneItems = Map.copyOf(standaloneItems); + } + + /** Materials and standalone blocks, without standalone items. */ + public Definitions( + java.util.Collection materials, + Map standaloneBlocks) { + this(materials, standaloneBlocks, Map.of()); + } + } + + /** + * Supplies {@link Definitions} built in code. The definition loader calls it once, after every + * extension file and data module is installed and before any material file is decoded, so it may + * register item and block modules and callbacks on {@code api} that material files then use. + */ + @FunctionalInterface + public interface DefinitionSource { + /** Registers any modules and callbacks on {@code api} and returns the definitions. */ + Definitions load(MaterialExtensions api); + } + + /** + * A registered definition source. + * + * @param name names the source in loading errors, such as {@code "CraftTweaker scripts"} + * @param source the source + */ + public record NamedSource(String name, DefinitionSource source) { + /** Checks that both values are present and the name is not blank. */ + public NamedSource { + Objects.requireNonNull(source); + if (name.isBlank()) throw new IllegalArgumentException("Definition source needs a name"); + } + } + /** * An immutable copy of the registered item and block parts at one moment. Decoding takes a * snapshot, so a definition decoded earlier is not changed by later registrations. @@ -80,6 +136,8 @@ public PartRegistry parts() { private final ConcurrentHashMap items = new ConcurrentHashMap<>(); private final ConcurrentHashMap blocks = new ConcurrentHashMap<>(); private final MaterialEvents events = new MaterialEvents(); + private final java.util.List definitionSources = + new java.util.concurrent.CopyOnWriteArrayList<>(); private final ConcurrentHashMap> itemImplementations = new ConcurrentHashMap<>(); private final ConcurrentHashMap> blockImplementations = @@ -393,6 +451,24 @@ public MaterialEvents events() { return events; } + /** + * Adds code-built definitions to definition loading. Register during mod construction. The + * loader calls each source once (see {@link DefinitionSource}) and plans what it returns with the + * same checks as definition files. Content outside MMDLib's namespace is only registered by its + * own mod's {@link zone.moddev.mc.lib.common.runtime.RuntimeContentRegistrar}. + * + * @param name names the source in loading errors + * @param source supplies the definitions + */ + public void registerDefinitionSource(String name, DefinitionSource source) { + definitionSources.add(new NamedSource(name, source)); + } + + /** The registered definition sources, in registration order. */ + public java.util.List definitionSources() { + return java.util.List.copyOf(definitionSources); + } + /** * Registers a code-defined item part. * diff --git a/src/main/java/zone/moddev/mc/lib/client/RuntimeContentClient.java b/src/main/java/zone/moddev/mc/lib/client/RuntimeContentClient.java index e40e1f8f..aea8a5ad 100644 --- a/src/main/java/zone/moddev/mc/lib/client/RuntimeContentClient.java +++ b/src/main/java/zone/moddev/mc/lib/client/RuntimeContentClient.java @@ -37,6 +37,7 @@ public static void items(RegisterColorHandlersEvent.Item event) { // Fluid container models obtain their tint from the fluid type, not the bucket // shell. if (!(item instanceof BucketItem) + && material.runtime().tinted() && !StaticAssetRules.INSTANCE.untintedItems().contains(id)) event.register( (stack, layer) -> layer == 0 ? material.properties().color() : -1, item); @@ -51,7 +52,8 @@ public static void blocks(RegisterColorHandlersEvent.Block event) { .blockMaterials() .forEach( (id, material) -> { - if (!StaticAssetRules.INSTANCE.untintedBlocks().contains(id)) + if (material.runtime().tinted() + && !StaticAssetRules.INSTANCE.untintedBlocks().contains(id)) event.register( (state, level, pos, tint) -> material.properties().color(), registrar.blocks().get(id).get()); @@ -114,7 +116,10 @@ public int getTintColor() { .values() .forEach( holder -> { + var owner = registrar.itemMaterials().get(holder.getId()); + // Precolored shields use their own item model instead of the vanilla shield. if (holder.get() instanceof ShieldItem + && (owner == null || owner.runtime().tinted()) && !StaticAssetRules.INSTANCE.untintedItems().contains(holder.getId())) event.registerItem( new IClientItemExtensions() { diff --git a/src/main/java/zone/moddev/mc/lib/common/loading/DefinitionLoader.java b/src/main/java/zone/moddev/mc/lib/common/loading/DefinitionLoader.java index c578f15c..eb217c31 100644 --- a/src/main/java/zone/moddev/mc/lib/common/loading/DefinitionLoader.java +++ b/src/main/java/zone/moddev/mc/lib/common/loading/DefinitionLoader.java @@ -25,8 +25,9 @@ /** * Loads every definition MMDLib finds in mods' {@code mmdlib/} directories at startup. Files are * discovered and decoded in parallel; they are then installed in a fixed order so results do not - * depend on timing: extension files (event handlers), then modules, then materials, then standalone - * blocks and items. See {@code docs/definition-loading.md}. + * depend on timing: extension files (event handlers), then modules, then code-built definitions + * from {@link MaterialExtensions#registerDefinitionSource} (which may add modules), then + * materials, then standalone blocks and items from files and sources. See {@code docs/definition-loading.md}. */ public final class DefinitionLoader { private DefinitionLoader() {} @@ -150,37 +151,51 @@ public static Loaded load( (id, source) -> install(source, () -> api.registerBlockModule(id, source.value()))); all.fluids.forEach( (id, source) -> install(source, () -> api.registerFluidModule(id, source.value()))); + // Code sources run once, now: modules and callbacks they register are visible to every + // material decoded below, from files as well as from the sources themselves. + var sourced = new LinkedHashMap(); + for (var source : api.definitionSources()) { + var origin = "definition source '" + source.name() + "'"; + sourced.put(origin, Objects.requireNonNull(source.source().load(api), origin)); + } var factory = new MaterialFactory(api, installed); var materials = new TreeMap(); - var concreteIds = new HashMap>(); + var concreteIds = new HashMap>(); + var materialOrigins = new HashMap(); all.materials.forEach( (id, source) -> { try { var result = factory.load(id, source.path()); validateConcreteIds(result, source.path(), concreteIds); - var plan = result.content(); - var bindings = - new java.util.LinkedHashMap>(); - java.util.stream.Stream.of(plan.items().values(), plan.extraItems().values()) - .flatMap(java.util.Collection::stream) - .forEach(item -> bindings.put(item.name(), item.events())); - java.util.stream.Stream.of(plan.blocks().values(), plan.extraBlocks().values()) - .flatMap(java.util.Collection::stream) - .forEach(block -> bindings.put(block.name(), block.events())); - bindings.forEach( - (name, events) -> { - try { - api.events().validate(events); - } catch (IllegalArgumentException ex) { - throw new IllegalArgumentException( - source.path() + ": " + name + ": " + ex.getMessage(), ex); - } - }); + validateBindings(result, source.path(), api); materials.put(id, result); + materialOrigins.put(id, source.path()); } catch (IOException ex) { throw new UncheckedIOException("Cannot read material " + source.path(), ex); } }); + for (var entry : sourced.entrySet()) { + var origin = entry.getKey(); + for (var material : entry.getValue().materials()) { + var id = + material + .name() + .orElseThrow(() -> new IllegalArgumentException(origin + ": unnamed material")); + var previous = materialOrigins.putIfAbsent(id, origin); + if (previous != null) + throw new IllegalArgumentException( + "Duplicate material " + id + ": " + previous + " and " + origin); + MaterialFactory.Result result; + try { + result = factory.create(material); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException(origin + ": " + ex.getMessage(), ex); + } + validateConcreteIds(result, origin, concreteIds); + validateBindings(result, origin, api); + materials.put(id, result); + } + } var standalone = new TreeMap(); all.standalone.forEach( (id, source) -> @@ -199,6 +214,32 @@ public static Loaded load( .forEach(block -> api.events().validate(block.events())); standalone.put(id, result); })); + for (var entry : sourced.entrySet()) { + var origin = entry.getKey(); + new TreeMap<>(entry.getValue().standaloneBlocks()) + .forEach( + (id, definition) -> { + if (standalone.containsKey(id)) + throw new IllegalArgumentException( + "Duplicate standalone block " + id + ": a definition file and " + origin); + try { + var module = api.blockModule(definition.module()); + if (module.requiredMod().isPresent() + && !installed.test(module.requiredMod().get())) return; + var result = definition.resolve(id, module); + validateConcreteIds(result, origin, concreteIds); + result + .content() + .blocks() + .values() + .forEach(block -> api.events().validate(block.events())); + standalone.put(id, result); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException( + origin + ": " + id + ": " + ex.getMessage(), ex); + } + }); + } var standaloneItems = new TreeMap(); all.standaloneItems.forEach( (id, source) -> @@ -217,6 +258,32 @@ public static Loaded load( .forEach(item -> api.events().validate(item.events())); standaloneItems.put(id, result); })); + for (var entry : sourced.entrySet()) { + var origin = entry.getKey(); + new TreeMap<>(entry.getValue().standaloneItems()) + .forEach( + (id, definition) -> { + if (standaloneItems.containsKey(id)) + throw new IllegalArgumentException( + "Duplicate standalone item " + id + ": a definition file and " + origin); + try { + var module = api.itemModule(definition.module()); + if (module.requiredMod().isPresent() + && !installed.test(module.requiredMod().get())) return; + var result = definition.resolve(id, module); + validateConcreteIds(result, origin, concreteIds); + result + .content() + .items() + .values() + .forEach(item -> api.events().validate(item.events())); + standaloneItems.put(id, result); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException( + origin + ": " + id + ": " + ex.getMessage(), ex); + } + }); + } return new Loaded( new ModuleCatalog(values(all.items), values(all.blocks), values(all.fluids)), values(all.extensions), @@ -332,10 +399,31 @@ private static Map immutableSorted(Map(values)); } + /** Checks every planned item's and block's event bindings against the callback catalog. */ + private static void validateBindings( + MaterialFactory.Result result, Object source, MaterialExtensions api) { + var plan = result.content(); + var bindings = new LinkedHashMap>(); + java.util.stream.Stream.of(plan.items().values(), plan.extraItems().values()) + .flatMap(Collection::stream) + .forEach(item -> bindings.put(item.name(), item.events())); + java.util.stream.Stream.of(plan.blocks().values(), plan.extraBlocks().values()) + .flatMap(Collection::stream) + .forEach(block -> bindings.put(block.name(), block.events())); + bindings.forEach( + (name, events) -> { + try { + api.events().validate(events); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException(source + ": " + name + ": " + ex.getMessage(), ex); + } + }); + } + private static void validateConcreteIds( MaterialFactory.Result result, - Path source, - Map> registries) { + Object source, + Map> registries) { var plan = result.content(); plan.itemDefinitions().keySet().forEach(id -> unique(registries, "item", id, source)); plan.blockDefinitions().keySet().forEach(id -> unique(registries, "block", id, source)); @@ -347,10 +435,10 @@ private static void validateConcreteIds( } private static void unique( - Map> registries, + Map> registries, String registry, ResourceLocation id, - Path source) { + Object source) { var previous = registries.computeIfAbsent(registry, key -> new HashMap<>()).putIfAbsent(id, source); if (previous != null) diff --git a/src/main/java/zone/moddev/mc/lib/common/types/MaterialRuntimeSettings.java b/src/main/java/zone/moddev/mc/lib/common/types/MaterialRuntimeSettings.java index 52a17378..67ea2cd6 100644 --- a/src/main/java/zone/moddev/mc/lib/common/types/MaterialRuntimeSettings.java +++ b/src/main/java/zone/moddev/mc/lib/common/types/MaterialRuntimeSettings.java @@ -7,7 +7,12 @@ import net.minecraft.resources.ResourceLocation; -/** Registry references used by runtime implementations, supplied by material data. */ +/** + * Registry references used by runtime implementations, supplied by material data. + * + * @param tinted whether the client tints the material's items and blocks with its color; false + * for artwork that is already colored + */ public record MaterialRuntimeSettings( Optional repairItem, ResourceLocation incorrectBlocks, @@ -16,7 +21,8 @@ public record MaterialRuntimeSettings( ResourceLocation armorTexture, Optional blockSound, Optional mapColor, - boolean tintedArmor) { + boolean tintedArmor, + boolean tinted) { public static final MaterialRuntimeSettings DEFAULT = new MaterialRuntimeSettings( Optional.empty(), @@ -26,7 +32,31 @@ public record MaterialRuntimeSettings( ResourceLocation.parse("minecraft:iron"), Optional.empty(), Optional.empty(), - false); + false, + true); + + /** Settings whose items and blocks are tinted with the material color. */ + public MaterialRuntimeSettings( + Optional repairItem, + ResourceLocation incorrectBlocks, + ResourceLocation baseBlock, + ResourceLocation blockSetType, + ResourceLocation armorTexture, + Optional blockSound, + Optional mapColor, + boolean tintedArmor) { + this( + repairItem, + incorrectBlocks, + baseBlock, + blockSetType, + armorTexture, + blockSound, + mapColor, + tintedArmor, + true); + } + public static final Codec CODEC = RecordCodecBuilder.create( i -> @@ -54,6 +84,9 @@ public record MaterialRuntimeSettings( .forGetter(MaterialRuntimeSettings::mapColor), Codec.BOOL .optionalFieldOf("tinted_armor", false) - .forGetter(MaterialRuntimeSettings::tintedArmor)) + .forGetter(MaterialRuntimeSettings::tintedArmor), + Codec.BOOL + .optionalFieldOf("tinted", true) + .forGetter(MaterialRuntimeSettings::tinted)) .apply(i, MaterialRuntimeSettings::new)); } diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/CraftTweakerInit.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/CraftTweakerInit.java new file mode 100644 index 00000000..5c42b8ea --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/CraftTweakerInit.java @@ -0,0 +1,26 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import net.neoforged.bus.api.IEventBus; + +import zone.moddev.mc.lib.api.MaterialExtensions; + +/** + * The only entry point into MMDLib's CraftTweaker integration. MMDLib calls {@link #register} from + * its constructor, and only when CraftTweaker is loaded, so no CraftTweaker class is touched + * without it. + */ +public final class CraftTweakerInit { + private CraftTweakerInit() {} + + /** + * Adds script-built materials, blocks and block modules to MMDLib's definition loading. The scripts run when MMDLib loads + * definitions: after every mod has constructed, so CraftTweaker is ready, and before registry + * events, so their content can still be registered. Their models, textures, names, loot + * tables and tags come from {@link MaterialPacks}. + */ + public static void register(IEventBus modBus) { + MaterialExtensions.GLOBAL.registerDefinitionSource( + "CraftTweaker scripts", ScriptDefinitions::load); + MaterialPacks.register(modBus); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MMDLibCraftTweakerPlugin.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MMDLibCraftTweakerPlugin.java new file mode 100644 index 00000000..4c623827 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MMDLibCraftTweakerPlugin.java @@ -0,0 +1,34 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.plugin.CraftTweakerPlugin; +import com.blamejared.crafttweaker.api.plugin.ICraftTweakerPlugin; +import com.blamejared.crafttweaker.api.plugin.ILoaderRegistrationHandler; +import com.blamejared.crafttweaker.api.plugin.IScriptLoadSourceRegistrationHandler; +import com.blamejared.crafttweaker.api.plugin.IScriptRunModuleConfiguratorRegistrationHandler; +import com.blamejared.crafttweaker.api.zencode.scriptrun.IScriptRunModuleConfigurator; + +/** + * Registers MMDLib's script loader and load source with CraftTweaker. CraftTweaker finds this class + * by its annotation while it constructs, so nothing in MMDLib refers to it. + */ +@CraftTweakerPlugin("mmdlib:main") +public final class MMDLibCraftTweakerPlugin implements ICraftTweakerPlugin { + @Override + public void registerLoaders(ILoaderRegistrationHandler handler) { + // Inheriting crafttweaker makes its types (IData, item stacks, entities...) usable in + // #loader mmdlib scripts. + handler.registerLoader(ScriptDefinitions.LOADER, "crafttweaker"); + } + + @Override + public void registerModuleConfigurators(IScriptRunModuleConfiguratorRegistrationHandler handler) { + // Every loader needs one; the default gives scripts CraftTweaker's standard modules. + handler.registerConfigurator( + ScriptDefinitions.LOADER, IScriptRunModuleConfigurator.createDefault("crafttweaker")); + } + + @Override + public void registerLoadSource(IScriptLoadSourceRegistrationHandler handler) { + handler.registerLoadSource(ScriptDefinitions.LOAD_SOURCE); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MaterialPacks.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MaterialPacks.java new file mode 100644 index 00000000..7d9e2fd7 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MaterialPacks.java @@ -0,0 +1,215 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.SortedMap; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.google.common.base.Suppliers; + +import net.minecraft.SharedConstants; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.AbstractPackResources; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.PackSelectionConfig; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.PathPackResources; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackSource; +import net.minecraft.server.packs.resources.IoSupplier; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModList; +import net.neoforged.neoforge.event.AddPackFindersEvent; + +import zone.moddev.mc.lib.MMDLib; + +/** + * Adds two always-enabled packs, for both resources and data: + * + *
    + *
  1. {@code mmdlib/script_materials}: what {@link ScriptMaterialResources} generates for the + * script materials. + *
  2. {@code mmdlib/mmdlibdata}: the {@code scripts/mmdlibdata} folder, a normal pack root + * ({@code assets/}, {@code data/}, optional {@code pack.mcmeta}) placed above the generated + * pack, so any generated file can be replaced. Its {@code templates/} folder replaces MMDLib's + * blank templates instead. + *
+ */ +final class MaterialPacks { + private MaterialPacks() {} + + /** Adds the packs whenever a pack repository is built. */ + static void register(IEventBus modBus) { + modBus.addListener(MaterialPacks::addPacks); + } + + /** {@code scripts/mmdlibdata}: pack authors' overrides for script materials. */ + static Path dataDirectory() { + return com.blamejared.crafttweaker.api.CraftTweakerAPI.getScriptsDirectory() + .resolve("mmdlibdata"); + } + + private static void addPacks(AddPackFindersEvent event) { + var type = event.getPackType(); + var folder = dataDirectory(); + var selection = new PackSelectionConfig(true, Pack.Position.TOP, false); + Supplier> contents = + Suppliers.memoize( + () -> { + var materials = ScriptDefinitions.materialResults(); + var blocks = ScriptDefinitions.blockResults(); + var items = ScriptDefinitions.itemResults(); + return type == PackType.CLIENT_RESOURCES + ? ScriptMaterialResources.assets( + materials, blocks, items, ScriptDefinitions.basedOn(), templates(folder), folder) + : ScriptMaterialResources.data( + materials, blocks, items, ScriptDefinitions.itemTags()); + }); + var generated = + new PackLocationInfo( + "mmdlib/script_materials", + Component.literal("MMDLib script materials"), + PackSource.BUILT_IN, + Optional.empty()); + event.addRepositorySource( + add -> + add.accept( + Pack.readMetaAndCreate( + generated, + supplier(info -> new GeneratedPack(info, type, contents)), + type, + selection))); + if (Files.isDirectory(folder)) { + var user = + new PackLocationInfo( + "mmdlib/mmdlibdata", + Component.literal("scripts/mmdlibdata"), + PackSource.BUILT_IN, + Optional.empty()); + event.addRepositorySource( + add -> + add.accept( + Pack.readMetaAndCreate( + user, + supplier(info -> new FolderPack(info, folder, type)), + type, + selection))); + } + } + + private static MaterialTemplates templates(Path folder) { + var jar = ModList.get().getModFileById(MMDLib.MODID).getFile().findResource("mmdlib_templates"); + return new MaterialTemplates(List.of(folder.resolve("templates"), jar)); + } + + private static Pack.ResourcesSupplier supplier( + java.util.function.Function open) { + return new Pack.ResourcesSupplier() { + @Override + public PackResources openPrimary(PackLocationInfo info) { + return open.apply(info); + } + + @Override + public PackResources openFull(PackLocationInfo info, Pack.Metadata metadata) { + return open.apply(info); + } + }; + } + + private static IoSupplier metadata(PackType type, String description) { + var json = + "{\"pack\":{\"description\":\"" + + description + + "\",\"pack_format\":" + + SharedConstants.getCurrentVersion().getPackVersion(type) + + "}}"; + var bytes = json.getBytes(StandardCharsets.UTF_8); + return () -> new ByteArrayInputStream(bytes); + } + + /** A pack whose files are generated in memory on first use. */ + private static final class GeneratedPack extends AbstractPackResources { + private final PackType type; + private final Supplier> contents; + + GeneratedPack( + PackLocationInfo info, PackType type, Supplier> contents) { + super(info); + this.type = type; + this.contents = contents; + } + + @Override + public IoSupplier getRootResource(String... path) { + return String.join("/", path).equals(PACK_META) + ? metadata(type, "Generated resources for MMDLib script materials") + : null; + } + + @Override + public IoSupplier getResource(PackType requested, ResourceLocation id) { + if (requested != type) return null; + var bytes = + contents.get().get(type.getDirectory() + "/" + id.getNamespace() + "/" + id.getPath()); + return bytes == null ? null : () -> new ByteArrayInputStream(bytes); + } + + @Override + public void listResources( + PackType requested, String namespace, String path, ResourceOutput output) { + if (requested != type) return; + var root = type.getDirectory() + "/" + namespace + "/"; + var prefix = root + path + "/"; + for (var entry : contents.get().tailMap(prefix).entrySet()) { + if (!entry.getKey().startsWith(prefix)) break; + var bytes = entry.getValue(); + output.accept( + ResourceLocation.fromNamespaceAndPath(namespace, entry.getKey().substring(root.length())), + () -> new ByteArrayInputStream(bytes)); + } + } + + @Override + public Set getNamespaces(PackType requested) { + if (requested != type) return Set.of(); + var root = type.getDirectory() + "/"; + return contents.get().keySet().stream() + .filter(key -> key.startsWith(root)) + .map(key -> key.substring(root.length(), key.indexOf('/', root.length()))) + .collect(Collectors.toUnmodifiableSet()); + } + + @Override + public void close() {} + } + + /** The {@code scripts/mmdlibdata} folder; it works without a {@code pack.mcmeta}. */ + private static final class FolderPack extends PathPackResources { + private final Path root; + private final PackType type; + + FolderPack(PackLocationInfo info, Path root, PackType type) { + super(info, root); + this.root = root; + this.type = type; + } + + @Override + public IoSupplier getRootResource(String... path) { + if (String.join("/", path).equals(PACK_META) && !Files.isRegularFile(root.resolve(PACK_META))) + return metadata(type, "Script material overrides from scripts/mmdlibdata"); + return super.getRootResource(path); + } + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MaterialTemplates.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MaterialTemplates.java new file mode 100644 index 00000000..8b379246 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/MaterialTemplates.java @@ -0,0 +1,46 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +/** + * The blank material templates: blockstates, models and grayscale textures whose names contain + * {@link #TOKEN}. Each file is read from the first root that has it, so a pack author can replace + * any template, for every script material at once, by putting a file at the same path under + * {@code scripts/mmdlibdata/templates/}. MMDLib's own templates are in its jar under {@code + * mmdlib_templates/}; {@code scripts/generate_material_templates.py} builds them. + */ +final class MaterialTemplates { + /** Stands for the material's name (its ID path) in template paths and contents. */ + static final String TOKEN = "__material__"; + + private final List roots; + + /** + * @param roots template roots, highest priority first; missing directories are skipped + */ + MaterialTemplates(List roots) { + this.roots = List.copyOf(roots); + } + + /** + * The template at {@code path}, relative to a template root, such as {@code + * "textures/item/__material___ingot.png"}. + */ + Optional read(String path) { + for (var root : roots) { + var file = root.resolve(path); + if (Files.isRegularFile(file)) + try { + return Optional.of(Files.readAllBytes(file)); + } catch (IOException ex) { + throw new UncheckedIOException("Cannot read material template " + file, ex); + } + } + return Optional.empty(); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockBuilder.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockBuilder.java new file mode 100644 index 00000000..96107f18 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockBuilder.java @@ -0,0 +1,137 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.Optional; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; +import com.blamejared.crafttweaker.api.data.IData; + +import net.minecraft.resources.ResourceLocation; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.common.types.FactoryData; +import zone.moddev.mc.lib.common.types.MaterialRuntimeSettings; +import zone.moddev.mc.lib.common.types.StandaloneBlock; + +/** + * Builds one block with its own ID from a {@code #loader mmdlib} script, like a {@code + * mmdlib/standalone/block} file. The module decides what kind of block it is: {@code mmdlib:block} + * (the default) for a plain cube, {@code mmdlib:stairs}, {@code mmdlib:door} and the other block + * parts, a module another mod registers, or a {@link ScriptBlockModule}. Values start from the + * module's template; the block is also put in {@code minecraft:mineable/pickaxe}. + * + *
{@code
+ * #loader mmdlib
+ * import mods.mmdlib.BlockBuilder;
+ *
+ * var marble = new BlockBuilder("marble_stairs", "mmdlib:stairs");
+ * marble.hardness = 1.5;
+ * marble.sound = "stone";
+ * marble.build();
+ * }
+ * + *

The block is {@code mmdlib:}. MMDLib generates its blockstate, models, name, loot table + * and tags; its textures must come from {@code scripts/mmdlibdata}. The log lists the textures each + * block needs. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.BlockBuilder") +public final class ScriptBlockBuilder extends ScriptBlockProperties { + private final ResourceLocation id; + private final ResourceLocation module; + private String baseBlock = MaterialRuntimeSettings.DEFAULT.baseBlock().toString(); + private String blockSetType = MaterialRuntimeSettings.DEFAULT.blockSetType().toString(); + private FactoryData blockEntityData = FactoryData.EMPTY; + + /** Starts a plain block named {@code name}, a path such as {@code "marble"}. */ + @ZenCodeType.Constructor + public ScriptBlockBuilder(String name) { + this(name, "mmdlib:block"); + } + + /** Starts a block named {@code name} built by {@code module}, such as {@code mmdlib:stairs}. */ + @ZenCodeType.Constructor + public ScriptBlockBuilder(String name, String module) { + this(mmdlibId(name, "block"), ResourceLocation.parse(module)); + } + + private ScriptBlockBuilder(ResourceLocation id, ResourceLocation module) { + super(ScriptDefinitions.catalog().blockModule(module).template(), FactoryData.EMPTY); + this.id = id; + this.module = module; + addTags("minecraft:mineable/pickaxe"); + } + + /** The block's ID, such as {@code mmdlib:marble}. */ + @ZenCodeType.Getter("name") + public String getName() { + return id.toString(); + } + + /** The module that builds the block. */ + @ZenCodeType.Getter("module") + public String getModule() { + return module.toString(); + } + + /** The block stairs copy their behavior from; default {@code minecraft:iron_block}. */ + @ZenCodeType.Getter("baseBlock") + public String getBaseBlock() { + return baseBlock; + } + + @ZenCodeType.Setter("baseBlock") + public void setBaseBlock(String baseBlock) { + this.baseBlock = ResourceLocation.parse(baseBlock).toString(); + } + + /** + * How doors, trapdoors, buttons and pressure plates open and sound, such as {@code minecraft:oak} + * (opened by hand) or {@code minecraft:iron} (needs redstone, the default). + */ + @ZenCodeType.Getter("blockSetType") + public String getBlockSetType() { + return blockSetType; + } + + @ZenCodeType.Setter("blockSetType") + public void setBlockSetType(String blockSetType) { + this.blockSetType = ResourceLocation.parse(blockSetType).toString(); + } + + /** Settings for the block entity, for modules that have one (machines). */ + @ZenCodeType.Setter("blockEntityData") + public void setBlockEntityData(IData data) { + var json = ScriptData.json(data); + if (!json.isJsonObject()) + throw new IllegalArgumentException("blockEntityData must be a map, such as {speed: 2}"); + blockEntityData = new FactoryData(json); + } + + /** + * Finishes the block and queues it for registration. + * + * @throws IllegalArgumentException if a value is invalid + * @throws IllegalStateException if the name was already built + */ + @ZenCodeType.Method + @Override + public void build() { + var d = MaterialRuntimeSettings.DEFAULT; + var runtime = + new MaterialRuntimeSettings( + d.repairItem(), + d.incorrectBlocks(), + ResourceLocation.parse(baseBlock), + ResourceLocation.parse(blockSetType), + d.armorTexture(), + d.blockSound(), + d.mapColor(), + false, + // Script blocks use their own textures as drawn. + false); + ScriptDefinitions.add( + id, + new StandaloneBlock(module, factoryData(), Optional.of(template()), runtime, blockEntityData)); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModule.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModule.java new file mode 100644 index 00000000..55e5b536 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModule.java @@ -0,0 +1,121 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.List; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import net.minecraft.resources.ResourceLocation; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.common.modules.DataModules; +import zone.moddev.mc.lib.common.types.PartMetadata; + +/** + * Defines a new block module, a kind of block, from a {@code #loader mmdlib} script, like a {@code + * data//block} file. Once built it can be used like any other module: materials select + * it as a part (by its name, in script materials and material files) and {@link + * ScriptBlockBuilder} builds standalone blocks with it. + * + *

{@code
+ * #loader mmdlib
+ * import mods.mmdlib.BlockModule;
+ *
+ * var pillar = new BlockModule("pillar", "mmdlib:block");
+ * pillar.hardness = 2;
+ * pillar.addTags("minecraft:mineable/pickaxe");
+ * pillar.build();
+ * }
+ * + *

A module starts as a copy of the module it is based on: its Java factory, factory data, + * template and block entity, but not its aliases. Change {@code factory} to use another Java + * implementation. A material's {@code copper_pillar} gets generated art from the template of the + * module it is based on, or else of its factory; a module with neither needs its art supplied. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.BlockModule") +public final class ScriptBlockModule extends ScriptBlockProperties { + private final ResourceLocation id; + private final DataModules.Block base; + private ResourceLocation factory; + private boolean materialDefaults; + + /** Starts module {@code name} (a path such as {@code "pillar"}) as a copy of {@code mmdlib:block}. */ + @ZenCodeType.Constructor + public ScriptBlockModule(String name) { + this(name, "mmdlib:block"); + } + + /** Starts module {@code name} as a copy of module {@code basedOn}, such as {@code mmdlib:stairs}. */ + @ZenCodeType.Constructor + public ScriptBlockModule(String name, String basedOn) { + this(mmdlibId(name, "block module"), ResourceLocation.parse(basedOn)); + } + + private final ResourceLocation baseId; + + private ScriptBlockModule(ResourceLocation id, ResourceLocation baseId) { + this(id, baseId, ScriptDefinitions.catalog().blockModule(baseId)); + } + + private ScriptBlockModule(ResourceLocation id, ResourceLocation baseId, DataModules.Block base) { + super(base.template(), base.factoryData()); + this.id = id; + this.baseId = baseId; + this.base = base; + factory = base.factory(); + materialDefaults = base.materialDefaults(); + } + + /** The module's ID, such as {@code mmdlib:pillar}. */ + @ZenCodeType.Getter("name") + public String getName() { + return id.toString(); + } + + /** The Java implementation that builds the blocks, such as {@code mmdlib:stairs}. */ + @ZenCodeType.Getter("factory") + public String getFactory() { + return factory.toString(); + } + + @ZenCodeType.Setter("factory") + public void setFactory(String factory) { + var id = ResourceLocation.parse(factory); + ScriptDefinitions.catalog().blockImplementation(id); + this.factory = id; + } + + /** Whether a material's stats (hardness, harvest tags...) override the template's. */ + @ZenCodeType.Getter("materialDefaults") + public boolean getMaterialDefaults() { + return materialDefaults; + } + + @ZenCodeType.Setter("materialDefaults") + public void setMaterialDefaults(boolean materialDefaults) { + this.materialDefaults = materialDefaults; + } + + /** + * Registers the module. Script lines after this, and every material file, can use it. + * + * @throws IllegalArgumentException if a value is invalid or the name is taken + */ + @ZenCodeType.Method + @Override + public void build() { + var part = base.part(); + ScriptDefinitions.catalog() + .registerBlockModule( + id, + new DataModules.Block( + factory, + base.requiredMod(), + materialDefaults, + factoryData(), + template(), + new PartMetadata(List.of(), part.entityFactory(), part.entityData(), part.blockEntity()))); + ScriptDefinitions.module(id, baseId, java.util.Set.of()); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java new file mode 100644 index 00000000..965a0928 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java @@ -0,0 +1,236 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.Optional; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.common.codecs.BlockTemplate; +import zone.moddev.mc.lib.common.types.FactoryData; +import zone.moddev.mc.lib.common.types.MMDBlock; + +/** + * What a script block and a script block module share: the block's properties and whether it has + * an item, besides the tags, events and factory data of every definition. Values start from the + * module the builder is based on. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.BlockProperties") +public abstract class ScriptBlockProperties extends ScriptDefinitionProperties { + private final BlockTemplate base; + private float hardness; + private float blastResistance; + private float friction; + private float speedFactor; + private float jumpFactor; + private int lightLevel; + private boolean requiresCorrectTool; + private boolean collision; + private boolean occlusion; + private boolean randomTicks; + private boolean replaceable; + private String sound; + private String mapColor; + private boolean hasItem; + + ScriptBlockProperties(BlockTemplate base, FactoryData factoryData) { + super(base.tags(), base.events(), factoryData); + this.base = base; + var p = base.properties(); + hardness = p.hardness(); + blastResistance = p.blastResistance(); + friction = p.friction(); + speedFactor = p.speedFactor(); + jumpFactor = p.jumpFactor(); + lightLevel = p.lightLevel(); + requiresCorrectTool = p.requiresCorrectTool(); + collision = p.collision(); + occlusion = p.occlusion(); + randomTicks = p.randomTicks(); + replaceable = p.replaceable(); + sound = p.sound(); + mapColor = p.mapColor(); + hasItem = base.hasItem(); + } + + /** Time to mine; -1 makes the block unbreakable. */ + @ZenCodeType.Getter("hardness") + public float getHardness() { + return hardness; + } + + @ZenCodeType.Setter("hardness") + public void setHardness(float hardness) { + this.hardness = hardness; + } + + /** Resistance to explosions. */ + @ZenCodeType.Getter("blastResistance") + public float getBlastResistance() { + return blastResistance; + } + + @ZenCodeType.Setter("blastResistance") + public void setBlastResistance(float blastResistance) { + this.blastResistance = blastResistance; + } + + /** Slipperiness; ice is 0.98, most blocks 0.6. */ + @ZenCodeType.Getter("friction") + public float getFriction() { + return friction; + } + + @ZenCodeType.Setter("friction") + public void setFriction(float friction) { + this.friction = friction; + } + + /** Walking speed multiplier; soul sand is 0.4. */ + @ZenCodeType.Getter("speedFactor") + public float getSpeedFactor() { + return speedFactor; + } + + @ZenCodeType.Setter("speedFactor") + public void setSpeedFactor(float speedFactor) { + this.speedFactor = speedFactor; + } + + /** Jump height multiplier; honey is 0.5. */ + @ZenCodeType.Getter("jumpFactor") + public float getJumpFactor() { + return jumpFactor; + } + + @ZenCodeType.Setter("jumpFactor") + public void setJumpFactor(float jumpFactor) { + this.jumpFactor = jumpFactor; + } + + /** Light emitted, 0 to 15. */ + @ZenCodeType.Getter("lightLevel") + public int getLightLevel() { + return lightLevel; + } + + @ZenCodeType.Setter("lightLevel") + public void setLightLevel(int lightLevel) { + this.lightLevel = lightLevel; + } + + /** Whether the block only drops when mined with the right tool. */ + @ZenCodeType.Getter("requiresCorrectTool") + public boolean getRequiresCorrectTool() { + return requiresCorrectTool; + } + + @ZenCodeType.Setter("requiresCorrectTool") + public void setRequiresCorrectTool(boolean requiresCorrectTool) { + this.requiresCorrectTool = requiresCorrectTool; + } + + /** Whether entities collide with the block. */ + @ZenCodeType.Getter("collision") + public boolean getCollision() { + return collision; + } + + @ZenCodeType.Setter("collision") + public void setCollision(boolean collision) { + this.collision = collision; + } + + /** Whether the block hides the faces of its neighbors; false for glass-like blocks. */ + @ZenCodeType.Getter("occlusion") + public boolean getOcclusion() { + return occlusion; + } + + @ZenCodeType.Setter("occlusion") + public void setOcclusion(boolean occlusion) { + this.occlusion = occlusion; + } + + /** Whether the block receives random ticks. */ + @ZenCodeType.Getter("randomTicks") + public boolean getRandomTicks() { + return randomTicks; + } + + @ZenCodeType.Setter("randomTicks") + public void setRandomTicks(boolean randomTicks) { + this.randomTicks = randomTicks; + } + + /** Whether placing a block replaces it, like grass. */ + @ZenCodeType.Getter("replaceable") + public boolean getReplaceable() { + return replaceable; + } + + @ZenCodeType.Setter("replaceable") + public void setReplaceable(boolean replaceable) { + this.replaceable = replaceable; + } + + /** The sound type, such as {@code stone}, {@code wood} or {@code metal}. */ + @ZenCodeType.Getter("sound") + public String getSound() { + return sound; + } + + @ZenCodeType.Setter("sound") + public void setSound(String sound) { + this.sound = sound; + } + + /** The map color, such as {@code stone}, {@code wood} or {@code metal}. */ + @ZenCodeType.Getter("mapColor") + public String getMapColor() { + return mapColor; + } + + @ZenCodeType.Setter("mapColor") + public void setMapColor(String mapColor) { + this.mapColor = mapColor; + } + + /** Whether the block has an item form. */ + @ZenCodeType.Getter("hasItem") + public boolean getHasItem() { + return hasItem; + } + + @ZenCodeType.Setter("hasItem") + public void setHasItem(boolean hasItem) { + this.hasItem = hasItem; + } + + /** The template these values describe. */ + BlockTemplate template() { + return new BlockTemplate( + base.name(), + base.parent(), + base.type(), + new MMDBlock.Properties( + hardness, + blastResistance, + friction, + speedFactor, + jumpFactor, + lightLevel, + requiresCorrectTool, + collision, + occlusion, + randomTicks, + replaceable, + sound, + mapColor), + tags(), + events(), + hasItem, + hasItem ? base.item() : Optional.empty()); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptData.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptData.java new file mode 100644 index 00000000..9da15558 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptData.java @@ -0,0 +1,108 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.data.*; +import com.blamejared.crafttweaker.api.data.visitor.DataVisitor; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import net.minecraft.nbt.NumericTag; + +/** + * Converts script data such as {@code {speed: 2, players: true}} to JSON for {@code factory_data}. + * Booleans stay booleans, unlike a conversion through NBT. + */ +final class ScriptData implements DataVisitor { + private static final ScriptData INSTANCE = new ScriptData(); + + private ScriptData() {} + + static JsonElement json(IData data) { + return data.accept(INSTANCE); + } + + private static JsonElement number(IData data) { + return new JsonPrimitive(((NumericTag) data.getInternal()).getAsNumber()); + } + + private static JsonElement list(java.util.List values) { + var array = new JsonArray(); + values.forEach(value -> array.add(json(value))); + return array; + } + + @Override + public JsonElement visitBool(BoolData data) { + return new JsonPrimitive(((NumericTag) data.getInternal()).getAsByte() != 0); + } + + @Override + public JsonElement visitByteArray(ByteArrayData data) { + return list(data.asList()); + } + + @Override + public JsonElement visitByte(ByteData data) { + return number(data); + } + + @Override + public JsonElement visitDouble(DoubleData data) { + return number(data); + } + + @Override + public JsonElement visitFloat(FloatData data) { + return number(data); + } + + @Override + public JsonElement visitIntArray(IntArrayData data) { + return list(data.asList()); + } + + @Override + public JsonElement visitInt(IntData data) { + return number(data); + } + + @Override + public JsonElement visitList(ListData data) { + return list(data.asList()); + } + + @Override + public JsonElement visitLongArray(LongArrayData data) { + return list(data.asList()); + } + + @Override + public JsonElement visitLong(LongData data) { + return number(data); + } + + @Override + public JsonElement visitMap(MapData data) { + var object = new JsonObject(); + data.asMap().forEach((key, value) -> object.add(key, json(value))); + return object; + } + + @Override + public JsonElement visitShort(ShortData data) { + return number(data); + } + + @Override + public JsonElement visitString(StringData data) { + return new JsonPrimitive(data.getInternal().getAsString()); + } + + @Override + public JsonElement visitEmpty(EmptyData data) { + return JsonNull.INSTANCE; + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java new file mode 100644 index 00000000..e93befe7 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java @@ -0,0 +1,151 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; +import com.blamejared.crafttweaker.api.data.IData; +import com.blamejared.crafttweaker.api.data.converter.JSONConverter; + +import com.google.gson.JsonObject; + +import net.minecraft.resources.ResourceLocation; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.common.types.FactoryData; + +/** + * What every script block, item and module shares: tags, event bindings, and {@code factory_data} + * for its Java implementation. Collections have an add and a remove function; remove accepts + * {@code "all"}. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.DefinitionProperties") +public abstract class ScriptDefinitionProperties { + static final String ALL = "all"; + + private final Set tags; + private final Map events; + private JsonObject factoryData; + + ScriptDefinitionProperties( + Set tags, Map events, FactoryData factoryData) { + this.tags = new LinkedHashSet<>(tags); + this.events = new LinkedHashMap<>(events); + this.factoryData = + factoryData.json().isJsonObject() + ? factoryData.json().getAsJsonObject().deepCopy() + : new JsonObject(); + } + + /** Finishes the definition. Call it once. */ + @ZenCodeType.Method + public abstract void build(); + + /** Settings for the Java implementation, such as {@code {detects: "players"}}. */ + @ZenCodeType.Getter("factoryData") + public IData getFactoryData() { + return JSONConverter.convert(factoryData); + } + + @ZenCodeType.Setter("factoryData") + public void setFactoryData(IData data) { + var json = ScriptData.json(data); + if (!json.isJsonObject()) + throw new IllegalArgumentException("factoryData must be a map, such as {speed: 2}"); + factoryData = json.getAsJsonObject(); + } + + /** The tags, such as {@code minecraft:mineable/pickaxe} for a block. */ + @ZenCodeType.Getter("tags") + public String[] getTags() { + return tags.stream().map(ResourceLocation::toString).sorted().toArray(String[]::new); + } + + /** Adds tags. */ + @ZenCodeType.Method + public ScriptDefinitionProperties addTags(String... tags) { + for (var tag : tags) { + if (tag.equals(ALL)) + throw new IllegalArgumentException("\"all\" can only remove tags, not add them"); + this.tags.add(ResourceLocation.parse(tag)); + } + return this; + } + + /** Removes tags; {@code "all"} removes every tag. */ + @ZenCodeType.Method + public ScriptDefinitionProperties removeTags(String... tags) { + for (var tag : tags) + if (tag.equals(ALL)) this.tags.clear(); + else this.tags.remove(ResourceLocation.parse(tag)); + return this; + } + + /** The event bindings, as {@code event=callback}. */ + @ZenCodeType.Getter("events") + public String[] getEvents() { + return events.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .sorted() + .toArray(String[]::new); + } + + /** + * Binds a callback to an event, such as {@code addEvent("block-right-click", + * "mmdlib:open_menu")}; an existing binding for the event is replaced. + */ + @ZenCodeType.Method + public ScriptDefinitionProperties addEvent(String event, String callback) { + var id = ResourceLocation.parse(callback); + ScriptDefinitions.catalog().events().validate(Map.of(event, id)); + events.put(event, id); + return this; + } + + /** Removes event bindings by event name; {@code "all"} removes every binding. */ + @ZenCodeType.Method + public ScriptDefinitionProperties removeEvents(String... events) { + for (var event : events) + if (event.equals(ALL)) this.events.clear(); + else this.events.remove(event); + return this; + } + + /** The tags set so far. */ + Set tags() { + return Set.copyOf(tags); + } + + /** The event bindings set so far. */ + Map events() { + return Map.copyOf(events); + } + + /** The {@code factory_data} set so far. */ + FactoryData factoryData() { + return new FactoryData(factoryData.deepCopy()); + } + + /** + * {@code name} as an ID in MMDLib's namespace: {@code "marble"} or {@code "mmdlib:marble"}. + * + * @throws IllegalArgumentException for another namespace + */ + static ResourceLocation mmdlibId(String name, String what) { + var id = ResourceLocation.parse(name.contains(":") ? name : "mmdlib:" + name); + if (!id.getNamespace().equals("mmdlib")) + throw new IllegalArgumentException( + "Script " + + what + + "s are registered in the mmdlib namespace; use \"" + + id.getPath() + + "\" instead of \"" + + name + + "\""); + return id; + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitions.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitions.java new file mode 100644 index 00000000..2a5d8947 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitions.java @@ -0,0 +1,177 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import com.blamejared.crafttweaker.api.CraftTweakerAPI; +import com.blamejared.crafttweaker.api.zencode.scriptrun.ScriptRunConfiguration; + +import net.minecraft.resources.ResourceLocation; + +import zone.moddev.mc.lib.MMDLib; +import zone.moddev.mc.lib.api.MaterialExtensions; +import zone.moddev.mc.lib.common.factories.MaterialFactory; +import zone.moddev.mc.lib.common.types.MMDMaterial; +import zone.moddev.mc.lib.common.types.StandaloneBlock; +import zone.moddev.mc.lib.common.types.StandaloneItem; + +/** + * Runs the {@code #loader mmdlib} scripts once, as MMDLib's definition source, and collects what + * they build. Block and item modules are registered on the catalog as soon as a script builds them, so later + * script lines and every material file can use them. Building is only accepted while that run is + * in progress: registries are filled from its result, so anything built later could never be + * registered. + */ +final class ScriptDefinitions { + /** The loader scripts name with {@code #loader mmdlib}. */ + static final String LOADER = MMDLib.MODID; + + /** Identifies MMDLib's startup run to CraftTweaker. */ + static final ResourceLocation LOAD_SOURCE = + ResourceLocation.fromNamespaceAndPath(MMDLib.MODID, "item_generation"); + + private static final Map materials = new LinkedHashMap<>(); + private static final Map blocks = new LinkedHashMap<>(); + private static final Map items = new LinkedHashMap<>(); + private static final Map> itemTags = new LinkedHashMap<>(); + private static final Map basedOn = new LinkedHashMap<>(); + private static volatile Set materialIds = Set.of(); + private static volatile Set blockIds = Set.of(); + private static volatile Set itemIds = Set.of(); + private static volatile Map> builtItemTags = Map.of(); + private static volatile Map builtBasedOn = Map.of(); + private static MaterialExtensions catalog; + + private ScriptDefinitions() {} + + /** Runs the scripts against {@code api} and returns what they built. */ + static synchronized MaterialExtensions.Definitions load(MaterialExtensions api) { + var logger = CraftTweakerAPI.getLogger(MMDLib.MODID); + logger.info("Running MMDLib definition scripts"); + materials.clear(); + blocks.clear(); + items.clear(); + itemTags.clear(); + basedOn.clear(); + catalog = api; + try { + CraftTweakerAPI.getScriptRunManager() + .createScriptRun( + new ScriptRunConfiguration( + LOADER, LOAD_SOURCE, ScriptRunConfiguration.RunKind.EXECUTE)) + .execute(); + } catch (VirtualMachineError fatal) { + throw fatal; + } catch (Throwable ex) { + // execute() declares Throwable. CraftTweaker reports script errors itself; keep whatever + // was built before the failure. + logger.error("Error running MMDLib definition scripts", ex); + } finally { + catalog = null; + } + logger.info( + "MMDLib scripts built {} materials, {} blocks and {} items", + materials.size(), + blocks.size(), + items.size()); + materialIds = Set.copyOf(materials.keySet()); + blockIds = Set.copyOf(blocks.keySet()); + itemIds = Set.copyOf(items.keySet()); + builtItemTags = Map.copyOf(itemTags); + builtBasedOn = Map.copyOf(basedOn); + var result = new MaterialExtensions.Definitions(materials.values(), blocks, items); + materials.clear(); + blocks.clear(); + items.clear(); + return result; + } + + /** + * The catalog being loaded. + * + * @throws IllegalStateException outside the startup run + */ + static synchronized MaterialExtensions catalog() { + if (catalog == null) + throw new IllegalStateException( + "MMDLib definitions can only be built from #loader " + LOADER + " scripts at startup"); + return catalog; + } + + /** + * Records a built material. + * + * @throws IllegalStateException outside the startup run, or if the name was already built + */ + static synchronized void add(MMDMaterial material) { + catalog(); + var id = material.name().orElseThrow(); + if (materials.putIfAbsent(id, material) != null) + throw new IllegalStateException("Material " + id + " is already built"); + } + + /** + * Records a built standalone block. + * + * @throws IllegalStateException outside the startup run, or if the ID was already built + */ + static synchronized void add(ResourceLocation id, StandaloneBlock block) { + catalog(); + if (blocks.putIfAbsent(id, block) != null) + throw new IllegalStateException("Block " + id + " is already built"); + } + + /** + * Records a built standalone item and its item tags. + * + * @throws IllegalStateException outside the startup run, or if the ID was already built + */ + static synchronized void add(ResourceLocation id, StandaloneItem item, Set tags) { + catalog(); + if (items.putIfAbsent(id, item) != null) + throw new IllegalStateException("Item " + id + " is already built"); + itemTags.put(id, Set.copyOf(tags)); + } + + /** + * Records a script module: the module it copies, whose templates its parts use, and for item + * modules the item tags its parts get. + */ + static synchronized void module( + ResourceLocation id, ResourceLocation base, Set tags) { + catalog(); + basedOn.put(id, base); + if (!tags.isEmpty()) itemTags.put(id, Set.copyOf(tags)); + } + + /** Item tags of script items and item modules, by item or module ID. */ + static Map> itemTags() { + return builtItemTags; + } + + /** The module each script module was copied from. */ + static Map basedOn() { + return builtBasedOn; + } + + /** The loaded results of script items; empty before definitions load. */ + static List itemResults() { + var loaded = MMDLib.standaloneItems(); + return itemIds.stream().sorted().map(loaded::get).filter(Objects::nonNull).toList(); + } + + /** The loaded results of script materials; empty before definitions load. */ + static List materialResults() { + var loaded = MMDLib.loadedMaterials(); + return materialIds.stream().sorted().map(loaded::get).filter(Objects::nonNull).toList(); + } + + /** The loaded results of script blocks; empty before definitions load. */ + static List blockResults() { + var loaded = MMDLib.standaloneBlocks(); + return blockIds.stream().sorted().map(loaded::get).filter(Objects::nonNull).toList(); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java new file mode 100644 index 00000000..7acb6dfc --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java @@ -0,0 +1,103 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.Optional; +import java.util.Set; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import net.minecraft.resources.ResourceLocation; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.common.types.FactoryData; +import zone.moddev.mc.lib.common.types.MaterialRuntimeSettings; +import zone.moddev.mc.lib.common.types.StandaloneItem; + +/** + * Builds one item with its own ID from a {@code #loader mmdlib} script, like a {@code + * mmdlib/standalone/item} file. The module decides what kind of item it is: {@code mmdlib:ingot} + * (the default) for a plain item, another MMDLib item part such as {@code mmdlib:pickaxe} or + * {@code mmdlib:bow}, a module another mod registers, or a {@link ScriptItemModule}. Values start + * from the module's template. + * + *

{@code
+ * #loader mmdlib
+ * import mods.mmdlib.ItemBuilder;
+ *
+ * var wrench = new ItemBuilder("wrench");
+ * wrench.stackSize = 1;
+ * wrench.addTags("c:tools/wrench");
+ * wrench.build();
+ * }
+ * + *

The item is {@code mmdlib:}. MMDLib generates its model, name and tags; its textures + * must come from {@code scripts/mmdlibdata}. The log lists the textures each item needs. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ItemBuilder") +public final class ScriptItemBuilder extends ScriptItemProperties { + private final ResourceLocation id; + private final ResourceLocation module; + + /** Starts a plain item named {@code name}, a path such as {@code "wrench"}. */ + @ZenCodeType.Constructor + public ScriptItemBuilder(String name) { + this(name, "mmdlib:ingot"); + } + + /** Starts an item named {@code name} built by {@code module}, such as {@code mmdlib:sword}. */ + @ZenCodeType.Constructor + public ScriptItemBuilder(String name, String module) { + this(mmdlibId(name, "item"), ResourceLocation.parse(module)); + } + + private ScriptItemBuilder(ResourceLocation id, ResourceLocation module) { + super( + ScriptDefinitions.catalog().itemModule(module).template(), + FactoryData.EMPTY, + ScriptDefinitions.itemTags().getOrDefault(module, Set.of())); + this.id = id; + this.module = module; + } + + /** The item's ID, such as {@code mmdlib:wrench}. */ + @ZenCodeType.Getter("name") + public String getName() { + return id.toString(); + } + + /** The module that builds the item. */ + @ZenCodeType.Getter("module") + public String getModule() { + return module.toString(); + } + + /** + * Finishes the item and queues it for registration. + * + * @throws IllegalArgumentException if a value is invalid + * @throws IllegalStateException if the name was already built + */ + @ZenCodeType.Method + @Override + public void build() { + var d = MaterialRuntimeSettings.DEFAULT; + var runtime = + new MaterialRuntimeSettings( + d.repairItem(), + d.incorrectBlocks(), + d.baseBlock(), + d.blockSetType(), + d.armorTexture(), + d.blockSound(), + d.mapColor(), + false, + // Script items use their own textures as drawn. + false); + var template = template(); + // Validate now, so the error points at this script line. + template.toBuilder().setName(module).forMaterial(id).setRegistryName(id).build(); + ScriptDefinitions.add( + id, new StandaloneItem(module, factoryData(), Optional.of(template), runtime), tags()); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java new file mode 100644 index 00000000..f705674e --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java @@ -0,0 +1,106 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.List; +import java.util.Set; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import net.minecraft.resources.ResourceLocation; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.common.modules.DataModules; +import zone.moddev.mc.lib.common.types.PartMetadata; + +/** + * Defines a new item module, a kind of item, from a {@code #loader mmdlib} script, like a {@code + * data//item} file. Once built it can be used like any other module: materials select + * it as a part (by its name, in script materials and material files) and {@link ScriptItemBuilder} + * builds standalone items with it. + * + *

{@code
+ * #loader mmdlib
+ * import mods.mmdlib.ItemModule;
+ *
+ * var plate = new ItemModule("heavy_plate", "mmdlib:ingot");
+ * plate.stackSize = 16;
+ * plate.build();
+ * }
+ * + *

A module starts as a copy of the module it is based on: its Java factory, factory data, + * template and companion entity, but not its aliases. Change {@code factory} to use another Java + * implementation. Its tags are given to every material's part, such as {@code copper_heavy_plate}, + * which gets generated art from the template of the module it is based on, or else its factory's. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ItemModule") +public final class ScriptItemModule extends ScriptItemProperties { + private final ResourceLocation id; + private final ResourceLocation baseId; + private final DataModules.Item base; + private ResourceLocation factory; + + /** Starts module {@code name} (a path such as {@code "widget"}) as a copy of {@code mmdlib:ingot}. */ + @ZenCodeType.Constructor + public ScriptItemModule(String name) { + this(name, "mmdlib:ingot"); + } + + /** Starts module {@code name} as a copy of module {@code basedOn}, such as {@code mmdlib:sword}. */ + @ZenCodeType.Constructor + public ScriptItemModule(String name, String basedOn) { + this(mmdlibId(name, "item module"), ResourceLocation.parse(basedOn)); + } + + private ScriptItemModule(ResourceLocation id, ResourceLocation baseId) { + this(id, baseId, ScriptDefinitions.catalog().itemModule(baseId)); + } + + private ScriptItemModule(ResourceLocation id, ResourceLocation baseId, DataModules.Item base) { + super(base.template(), base.factoryData(), ScriptDefinitions.itemTags().getOrDefault(baseId, Set.of())); + this.id = id; + this.baseId = baseId; + this.base = base; + factory = base.factory(); + } + + /** The module's ID, such as {@code mmdlib:widget}. */ + @ZenCodeType.Getter("name") + public String getName() { + return id.toString(); + } + + /** The Java implementation that builds the items, such as {@code mmdlib:sword}. */ + @ZenCodeType.Getter("factory") + public String getFactory() { + return factory.toString(); + } + + @ZenCodeType.Setter("factory") + public void setFactory(String factory) { + var id = ResourceLocation.parse(factory); + ScriptDefinitions.catalog().itemImplementation(id); + this.factory = id; + } + + /** + * Registers the module. Script lines after this, and every material file, can use it. + * + * @throws IllegalArgumentException if a value is invalid or the name is taken + */ + @ZenCodeType.Method + @Override + public void build() { + var part = base.part(); + ScriptDefinitions.catalog() + .registerItemModule( + id, + new DataModules.Item( + factory, + base.requiredMod(), + factoryData(), + template(), + new PartMetadata(List.of(), part.entityFactory(), part.entityData(), part.blockEntity()))); + ScriptDefinitions.module(id, baseId, tags()); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java new file mode 100644 index 00000000..dfd03091 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java @@ -0,0 +1,107 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.Optional; +import java.util.Set; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import net.minecraft.resources.ResourceLocation; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.common.codecs.ItemTemplate; +import zone.moddev.mc.lib.common.types.FactoryData; + +/** + * What a script item and a script item module share: stack size, durability, fire resistance and + * container, besides the tags (item tags), events and factory data of every definition. Values + * start from the module the builder is based on. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ItemProperties") +public abstract class ScriptItemProperties extends ScriptDefinitionProperties { + private final ItemTemplate base; + private int stackSize; + private int durability; + private boolean fireResistant; + private Optional container; + + ScriptItemProperties(ItemTemplate base, FactoryData factoryData, Set tags) { + super(tags, base.events(), factoryData); + this.base = base; + stackSize = base.stackSize(); + durability = base.damage(); + fireResistant = base.fireResistant(); + container = base.container(); + } + + /** How many fit in one stack, 1 to 99. */ + @ZenCodeType.Getter("stackSize") + public int getStackSize() { + return stackSize; + } + + @ZenCodeType.Setter("stackSize") + public void setStackSize(int stackSize) { + this.stackSize = stackSize; + } + + /** Uses before the item breaks; 0 for none. A positive value also sets the stack size to 1. */ + @ZenCodeType.Getter("durability") + public int getDurability() { + return durability; + } + + @ZenCodeType.Setter("durability") + public void setDurability(int durability) { + this.durability = durability; + if (durability > 0) stackSize = 1; + } + + /** Whether the item survives fire and lava. */ + @ZenCodeType.Getter("fireResistant") + public boolean getFireResistant() { + return fireResistant; + } + + @ZenCodeType.Setter("fireResistant") + public void setFireResistant(boolean fireResistant) { + this.fireResistant = fireResistant; + } + + /** + * What is left in the crafting grid after the item is used in a recipe, such as {@code + * minecraft:bucket}; an empty string for nothing. + */ + @ZenCodeType.Getter("container") + public String getContainer() { + return container.map(ResourceLocation::toString).orElse(""); + } + + @ZenCodeType.Setter("container") + public void setContainer(String container) { + this.container = + container.isEmpty() ? Optional.empty() : Optional.of(ResourceLocation.parse(container)); + } + + /** The template these values describe. */ + ItemTemplate template() { + return new ItemTemplate( + base.name(), + base.parent(), + base.type(), + stackSize, + durability, + fireResistant, + base.food(), + container, + base.delayedUse(), + base.colorHandler(), + base.lore(), + base.actions(), + events(), + base.group(), + base.creativeStacks(), + base.attributes()); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialBuilder.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialBuilder.java new file mode 100644 index 00000000..4b628418 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialBuilder.java @@ -0,0 +1,340 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import net.minecraft.resources.ResourceLocation; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.MMDLib; +import zone.moddev.mc.lib.common.builders.MaterialBuilder; +import zone.moddev.mc.lib.common.types.MaterialPart; +import zone.moddev.mc.lib.common.types.MaterialProperties; +import zone.moddev.mc.lib.common.types.MaterialRuntimeSettings; +import zone.moddev.mc.lib.common.types.MaterialSelection; +import zone.moddev.mc.lib.common.types.PartSelection; + +/** + * Builds an MMDLib material from a {@code #loader mmdlib} script. Plain values are properties; + * collections have an add and a remove function, where the name {@code "all"} means every entry. + * Nothing is registered until {@link #build()}. + * + *

{@code
+ * #loader mmdlib
+ * import mods.mmdlib.MaterialBuilder;
+ *
+ * var copper = new MaterialBuilder("copper");
+ * copper.type = "metal";
+ * copper.color = 0xFFFF9B40;
+ * copper.hardness = 3.5;
+ * copper.removeItems("all").addItems("ingot", "nugget", "dust");
+ * copper.removeBlocks("door");
+ * copper.hasFluid = false;
+ * copper.build();
+ * }
+ * + *

A new builder matches a material file that sets nothing: every item and block part, and a + * fluid. Content is registered as {@code mmdlib:_}. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.MaterialBuilder") +public final class ScriptMaterialBuilder { + private static final String ALL = "all"; + + private final ResourceLocation name; + private final Set availableItems; + private final Set availableBlocks; + private final Set availableEntities; + private final Set items; + private final Set blocks; + private final Set harvestTags = new LinkedHashSet<>(); + private final Map special = new LinkedHashMap<>(); + private String type = MaterialProperties.DEFAULT.type(); + private int color = MaterialProperties.DEFAULT.color(); + private double hardness; + private double strength; + private double magic; + private double damage; + private boolean hasFluid; + private ResourceLocation fluid; + + /** + * Starts a material. The name is a path such as {@code "copper"}; {@code "mmdlib:copper"} is also + * accepted, but no other namespace. + */ + @ZenCodeType.Constructor + public ScriptMaterialBuilder(String name) { + var id = ResourceLocation.parse(name.contains(":") ? name : MMDLib.MODID + ":" + name); + if (!id.getNamespace().equals(MMDLib.MODID)) + throw new IllegalArgumentException( + "Script materials are registered in the " + + MMDLib.MODID + + " namespace; use \"" + + id.getPath() + + "\" instead of \"" + + name + + "\""); + this.name = id; + var snapshot = ScriptDefinitions.catalog().snapshot(); + var parts = snapshot.parts(); + var defaults = MaterialSelection.defaults(snapshot); + availableItems = parts.items(); + availableBlocks = parts.blocks(); + availableEntities = parts.entities(); + items = new LinkedHashSet<>(defaults.items()); + blocks = new LinkedHashSet<>(defaults.blocks()); + hasFluid = defaults.hasFluid(); + fluid = defaults.fluidModule(); + } + + /** The material's ID, such as {@code mmdlib:copper}. */ + @ZenCodeType.Getter("name") + public String getName() { + return name.toString(); + } + + /** The classification, such as {@code metal} or {@code gem}; default {@code generic}. */ + @ZenCodeType.Getter("type") + public String getType() { + return type; + } + + @ZenCodeType.Setter("type") + public void setType(String type) { + this.type = type; + } + + /** The ARGB color used to tint textures; default opaque white. */ + @ZenCodeType.Getter("color") + public int getColor() { + return color; + } + + @ZenCodeType.Setter("color") + public void setColor(int color) { + this.color = color; + } + + /** Mining speed, armor toughness and related values; default 0. */ + @ZenCodeType.Getter("hardness") + public double getHardness() { + return hardness; + } + + @ZenCodeType.Setter("hardness") + public void setHardness(double hardness) { + this.hardness = hardness; + } + + /** Tool and armor durability; default 0. */ + @ZenCodeType.Getter("strength") + public double getStrength() { + return strength; + } + + @ZenCodeType.Setter("strength") + public void setStrength(double strength) { + this.strength = strength; + } + + /** Magic affinity, which drives enchantability; default 0. */ + @ZenCodeType.Getter("magic") + public double getMagic() { + return magic; + } + + @ZenCodeType.Setter("magic") + public void setMagic(double magic) { + this.magic = magic; + } + + /** The tool tier's base attack-damage bonus; default 0. */ + @ZenCodeType.Getter("damage") + public double getDamage() { + return damage; + } + + @ZenCodeType.Setter("damage") + public void setDamage(double damage) { + this.damage = damage; + } + + /** Whether the material gets a fluid; default true. */ + @ZenCodeType.Getter("hasFluid") + public boolean getHasFluid() { + return hasFluid; + } + + @ZenCodeType.Setter("hasFluid") + public void setHasFluid(boolean hasFluid) { + this.hasFluid = hasFluid; + } + + /** The fluid module that builds the fluid; default {@code mmdlib:molten}. */ + @ZenCodeType.Getter("fluid") + public String getFluid() { + return fluid.toString(); + } + + @ZenCodeType.Setter("fluid") + public void setFluid(String fluid) { + this.fluid = ResourceLocation.parse(fluid); + } + + /** The selected item parts, by short ID. */ + @ZenCodeType.Getter("items") + public String[] getItems() { + return ids(items); + } + + /** Adds item parts by name, such as {@code "ingot"}; {@code "all"} adds every item part. */ + @ZenCodeType.Method + public ScriptMaterialBuilder addItems(String... names) { + items.addAll(resolve(names, availableItems, "item")); + return this; + } + + /** Removes item parts by name; {@code "all"} removes every item part. */ + @ZenCodeType.Method + public ScriptMaterialBuilder removeItems(String... names) { + items.removeAll(resolve(names, availableItems, "item")); + return this; + } + + /** The selected block parts, by short ID. */ + @ZenCodeType.Getter("blocks") + public String[] getBlocks() { + return ids(blocks); + } + + /** Adds block parts by name, such as {@code "block"}; {@code "all"} adds every block part. */ + @ZenCodeType.Method + public ScriptMaterialBuilder addBlocks(String... names) { + blocks.addAll(resolve(names, availableBlocks, "block")); + return this; + } + + /** Removes block parts by name; {@code "all"} removes every block part. */ + @ZenCodeType.Method + public ScriptMaterialBuilder removeBlocks(String... names) { + blocks.removeAll(resolve(names, availableBlocks, "block")); + return this; + } + + /** The harvest tags, such as {@code minecraft:needs_iron_tool}. */ + @ZenCodeType.Getter("harvestTags") + public String[] getHarvestTags() { + return harvestTags.stream().map(ResourceLocation::toString).toArray(String[]::new); + } + + /** + * Adds block tags the material's blocks are put in, such as {@code "minecraft:needs_iron_tool"}. + */ + @ZenCodeType.Method + public ScriptMaterialBuilder addHarvestTags(String... tags) { + for (var tag : tags) { + if (tag.equals(ALL)) + throw new IllegalArgumentException("\"all\" can only remove harvest tags, not add them"); + harvestTags.add(ResourceLocation.parse(tag)); + } + return this; + } + + /** Removes harvest tags; {@code "all"} removes every tag. */ + @ZenCodeType.Method + public ScriptMaterialBuilder removeHarvestTags(String... tags) { + for (var tag : tags) + if (tag.equals(ALL)) harvestTags.clear(); + else harvestTags.remove(ResourceLocation.parse(tag)); + return this; + } + + /** + * Sets a special stat, such as {@code blast_resistance}, {@code temperature} or {@code density}; + * an existing value is replaced. + */ + @ZenCodeType.Method + public ScriptMaterialBuilder addSpecial(String key, double value) { + if (key.equals(ALL)) throw new IllegalArgumentException("\"all\" is not a special stat name"); + special.put(key, value); + return this; + } + + /** Removes special stats by key; {@code "all"} removes every one. */ + @ZenCodeType.Method + public ScriptMaterialBuilder removeSpecial(String... keys) { + for (var key : keys) + if (key.equals(ALL)) special.clear(); + else special.remove(key); + return this; + } + + /** + * Finishes the material and queues it for registration. Call it once, from a {@code #loader + * mmdlib} script; later changes to this builder have no effect. + * + * @throws IllegalArgumentException if a value is invalid, such as a negative hardness + * @throws IllegalStateException if the name was already built + */ + @ZenCodeType.Method + public void build() { + var material = + MaterialBuilder.create() + .setName(name) + .setProperties( + new MaterialProperties( + type, + color, + hardness, + strength, + magic, + damage, + Set.copyOf(harvestTags), + Map.copyOf(special))) + // Projectile entities follow their items: a selected arrow always gets its entity. + .setSelection( + new MaterialSelection( + items, blocks, availableEntities, hasFluid, Set.of(), Set.of(), fluid)) + // Generated textures are colored already (see ScriptMaterialResources), and armor + // layers are generated under the material's own name. + .setRuntime(runtime()) + .build(); + ScriptDefinitions.add(material); + } + + private MaterialRuntimeSettings runtime() { + var d = MaterialRuntimeSettings.DEFAULT; + return new MaterialRuntimeSettings( + d.repairItem(), + d.incorrectBlocks(), + d.baseBlock(), + d.blockSetType(), + name, + d.blockSound(), + d.mapColor(), + false, + false); + } + + private static Set resolve( + String[] names, Set available, String section) { + for (var name : names) + if (name.startsWith("!")) + throw new IllegalArgumentException( + "Use remove instead of \"" + name + "\" to leave out a " + section + " part"); + try { + return PartSelection.parse(String.join(" ", names), available); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException("Unknown " + section + " part: " + ex.getMessage(), ex); + } + } + + private static String[] ids(Set parts) { + return parts.stream().map(MaterialPart::id).sorted().toArray(String[]::new); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java new file mode 100644 index 00000000..5ff957e6 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java @@ -0,0 +1,603 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.UnaryOperator; + +import javax.imageio.ImageIO; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import net.minecraft.resources.ResourceLocation; + +import zone.moddev.mc.lib.MMDLib; +import zone.moddev.mc.lib.common.factories.MaterialFactory; +import zone.moddev.mc.lib.common.types.MMDMaterial; +import zone.moddev.mc.lib.common.types.MaterialPart; + +/** + * Generates the resources script materials, blocks and items need and cannot get from a mod: blockstates, + * models, English names and (for materials) colored textures as assets, and block loot tables and + * tags as data. Assets come from {@link MaterialTemplates}, with {@link MaterialTemplates#TOKEN} + * replaced by the material's or block's name; material textures are multiplied by the material + * color. Recipes are left to scripts. + */ +final class ScriptMaterialResources { + private static final String T = MaterialTemplates.TOKEN; + private static final String NS = MMDLib.MODID; + private static final Set ARMOR = Set.of("helmet", "chestplate", "leggings", "boots"); + private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); + + private ScriptMaterialResources() {} + + /** + * Client resources for script materials and blocks, keyed by path in the pack ({@code + * assets/...}). Material textures are generated in the material color; block textures are not + * generated, and any the {@code userFolder} pack does not supply are logged. + * + * @throws IllegalStateException if a template references a template that does not exist + */ + static SortedMap assets( + Collection materials, + Collection blocks, + Collection items, + Map basedOn, + MaterialTemplates templates, + Path userFolder) { + var files = new TreeMap(); + var lang = new TreeMap(); + for (var result : materials) { + var material = result.material(); + var name = name(material); + var plan = result.content(); + // Output texture path -> template texture path. + var textures = new TreeMap(); + // A part without a template, such as a code-registered type from another mod, is skipped: + // its resources come from the mod that registers it. + plan.items() + .forEach( + (part, item) -> + templatePart(part, "models/item/", templates, basedOn) + .ifPresent( + tpl -> + expand( + "models/item/" + T + "_" + tpl, + materialNames(tpl, item.name().getPath(), name), + templates, + files, + textures))); + plan.blocks() + .forEach( + (part, block) -> + templatePart(part, "blockstates/", templates, basedOn) + .ifPresent( + tpl -> + expand( + "blockstates/" + T + "_" + tpl, + materialNames(tpl, block.name().getPath(), name), + templates, + files, + textures))); + if (plan.fluid() != null) { + expand("blockstates/" + T + "_molten", s -> s.replace(T, name), templates, files, textures); + expand( + "models/item/" + T + "_molten_bucket", s -> s.replace(T, name), templates, files, textures); + } + if (plan.items().keySet().stream().anyMatch(part -> ARMOR.contains(part.id()))) { + for (var layer : List.of("_layer_1", "_layer_2")) + textures.put( + "models/armor/" + name + layer, "models/armor/" + T + layer); + } + if (plan.items().keySet().stream().anyMatch(part -> part.id().equals("horse_armor"))) + textures.put("entity/horse/armor/horse_armor_" + name, "entity/horse/armor/horse_armor_" + T); + int color = material.properties().color(); + textures.forEach( + (out, texture) -> { + var tint = + templates + .read("textures/" + texture + ".png") + .orElseThrow(() -> new IllegalStateException("No texture template " + texture)); + var target = "assets/" + NS + "/textures/" + out + ".png"; + files.put( + target, + colorize(tint, templates.read("textures/" + texture + ".untinted.png"), color)); + templates + .read("textures/" + texture + ".png.mcmeta") + .ifPresent(meta -> files.put(target + ".mcmeta", meta)); + }); + var display = display(name); + plan.items() + .forEach( + (part, item) -> { + if (!plan.blocks().containsKey(part)) + lang.put("item." + NS + "." + item.name().getPath(), label(part.id(), display)); + }); + plan.blocks() + .forEach( + (part, block) -> + lang.put("block." + NS + "." + block.name().getPath(), label(part.id(), display))); + plan.entities() + .forEach( + (part, entity) -> + lang.put("entity." + NS + "." + entity.name().getPath(), label(part.id(), display))); + if (plan.fluid() != null) { + lang.put("fluid_type." + NS + "." + name + "_molten", "Molten " + display); + lang.put("item." + NS + "." + name + "_molten_bucket", "Molten " + display + " Bucket"); + } + } + for (var result : blocks) + result + .content() + .blocks() + .forEach( + (part, block) -> { + var id = block.name(); + lang.put("block." + id.getNamespace() + "." + id.getPath(), display(id.getPath())); + var tpl = templatePart(part, "blockstates/", templates, basedOn); + if (tpl.isEmpty()) { + noTemplate("block", id, part); + return; + } + var textures = new TreeMap(); + var names = ownNames(tpl.get(), id.getPath()); + expand("blockstates/" + T + "_" + tpl.get(), names, templates, files, textures); + if (block.item() != null) + expand("models/item/" + T + "_" + tpl.get(), names, templates, files, textures); + reportMissing("block", id, textures, userFolder); + }); + for (var result : items) + result + .content() + .items() + .forEach( + (part, item) -> { + var id = item.name(); + lang.put("item." + id.getNamespace() + "." + id.getPath(), display(id.getPath())); + var tpl = templatePart(part, "models/item/", templates, basedOn); + if (tpl.isEmpty()) { + noTemplate("item", id, part); + return; + } + var textures = new TreeMap(); + expand( + "models/item/" + T + "_" + tpl.get(), + ownNames(tpl.get(), id.getPath()), + templates, + files, + textures); + reportMissing("item", id, textures, userFolder); + }); + if (!lang.isEmpty()) + files.put("assets/" + NS + "/lang/en_us.json", json(lang).getBytes(StandardCharsets.UTF_8)); + return files; + } + + private static void noTemplate(String kind, ResourceLocation id, MaterialPart part) { + LOGGER.warn( + "Script {} {} uses {}, which has no MMDLib template; supply its models and textures in" + + " scripts/mmdlibdata", + kind, + id, + part.templateId()); + } + + private static void reportMissing( + String kind, ResourceLocation id, Map textures, Path userFolder) { + var missing = + textures.keySet().stream() + .map(out -> "assets/" + NS + "/textures/" + out + ".png") + .filter(path -> !Files.isRegularFile(userFolder.resolve(path))) + .toList(); + if (!missing.isEmpty()) + LOGGER.warn("Script {} {} needs these textures in {}: {}", kind, id, userFolder, missing); + } + + /** + * The template a part uses: its module's own ({@code __material___}) if MMDLib has one, else + * that of the module a script module was copied from (and so on), else its Java factory's. So a + * script module copied from {@code mmdlib:stairs} looks like stairs. + */ + private static Optional templatePart( + MaterialPart part, + String folder, + MaterialTemplates templates, + Map basedOn) { + var candidates = new ArrayList(); + var seen = new HashSet(); + for (var module = part.templateId(); module != null && seen.add(module); module = basedOn.get(module)) + if (module.getNamespace().equals(NS)) candidates.add(module.getPath()); + if (part.factory().getNamespace().equals(NS)) candidates.add(part.factory().getPath()); + return candidates.stream() + .filter(c -> templates.read("assets/" + NS + "/" + folder + T + "_" + c + ".json").isPresent()) + .findFirst(); + } + + /** Names a material part's files: {@code __material___stairs_inner} -> {@code copper_stairs_inner}. */ + private static UnaryOperator materialNames(String tpl, String output, String material) { + return s -> s.replace(T + "_" + tpl, output).replace(T, material); + } + + /** + * Names a standalone block's or item's files after its ID: {@code marble_stairs}, {@code + * marble_stairs_inner}, and the material block texture becomes the block's own texture. + */ + private static UnaryOperator ownNames(String tpl, String id) { + return s -> + s.replace(T + "_" + tpl, id) + .replace(T + "_block", id) + .replace(T + "_", id + "_") + .replace(T, id); + } + + /** + * Writes the template {@code root} and every template it references, renamed by {@code names}, + * into {@code files}, and collects the textures they use as output path -> template path. + */ + private static void expand( + String root, + UnaryOperator names, + MaterialTemplates templates, + Map files, + Map textures) { + var json = new ArrayDeque(List.of(root)); + var seen = new HashSet(); + while (!json.isEmpty()) { + var path = json.pop(); + if (!seen.add(path)) continue; + // Templates referenced from other templates must exist. + var text = + new String( + templates + .read("assets/" + NS + "/" + path + ".json") + .orElseThrow(() -> new IllegalStateException("No material template " + path)), + StandardCharsets.UTF_8); + var used = new TreeSet(); + references(JsonParser.parseString(text), null, json, used); + used.forEach(texture -> textures.put(names.apply(texture), texture)); + files.put( + "assets/" + NS + "/" + names.apply(path) + ".json", + names.apply(text).getBytes(StandardCharsets.UTF_8)); + } + } + + /** + * Server data for script materials and blocks: block loot tables and tags, keyed by path in the + * pack. + */ + static SortedMap data( + Collection materials, + Collection blocks, + Collection items, + Map> itemTags) { + var files = new TreeMap(); + var tags = new TreeMap>(); + for (var result : materials) { + var material = result.material(); + var name = name(material); + var plan = result.content(); + var properties = material.properties(); + plan.items() + .forEach( + (part, item) -> { + if (!plan.blocks().containsKey(part)) + itemTags(part.id(), name, item.name().toString(), tags); + // Tags a script item module gives its parts. + itemTags + .getOrDefault(part.templateId(), Set.of()) + .forEach(tag -> tag(tags, "item", tag.toString(), item.name().toString())); + }); + var hardness = properties.hardness(); + var level = + hardness >= 10 ? "diamond" : hardness >= 8 ? "iron" : hardness >= 5 ? "stone" : null; + plan.blocks() + .forEach( + (part, block) -> { + var id = block.name().toString(); + block.tags().forEach(tag -> tag(tags, "block", tag.toString(), id)); + properties.harvestTags().forEach(tag -> tag(tags, "block", tag.toString(), id)); + tag(tags, "block", "minecraft:mineable/pickaxe", id); + if (level != null) tag(tags, "block", "minecraft:needs_" + level + "_tool", id); + var hasSlab = + plan.blocks().keySet().stream().anyMatch(other -> other.id().equals("slab")); + lootTable(part.factory(), id, NS + ":" + name + "_slab", hasSlab) + .ifPresent( + table -> + files.put( + "data/" + NS + "/loot_table/blocks/" + block.name().getPath() + ".json", + table.getBytes(StandardCharsets.UTF_8))); + }); + if (plan.fluid() != null) { + var fluid = plan.fluid().name(); + tag(tags, "fluid", "c:molten_" + name, fluid.toString()); + tag(tags, "fluid", "c:molten_" + name, NS + ":flowing_" + fluid.getPath()); + } + } + for (var result : blocks) + result + .content() + .blocks() + .forEach( + (part, block) -> { + var id = block.name().toString(); + block.tags().forEach(tag -> tag(tags, "block", tag.toString(), id)); + lootTable(part.factory(), id, null, false) + .ifPresent( + table -> + files.put( + "data/" + block.name().getNamespace() + "/loot_table/blocks/" + + block.name().getPath() + ".json", + table.getBytes(StandardCharsets.UTF_8))); + }); + for (var result : items) + result + .content() + .items() + .values() + .forEach( + item -> + itemTags + .getOrDefault(item.name(), Set.of()) + .forEach(tag -> tag(tags, "item", tag.toString(), item.name().toString()))); + tags.forEach( + (key, values) -> { + var parts = key.split("\\|", 2); + var id = ResourceLocation.parse(parts[1]); + var body = new JsonObject(); + body.addProperty("replace", false); + var list = new JsonArray(); + values.forEach(list::add); + body.add("values", list); + files.put( + "data/" + id.getNamespace() + "/tags/" + parts[0] + "/" + id.getPath() + ".json", + json(body).getBytes(StandardCharsets.UTF_8)); + }); + return files; + } + + /** A material's name as used in its registry IDs: the path of its ID. */ + static String name(MMDMaterial material) { + return material.name().orElseThrow().getPath(); + } + + /** {@code cold_iron} as {@code Cold Iron}. */ + static String display(String name) { + var words = new ArrayList(); + for (var word : name.split("[_/]")) + if (!word.isEmpty()) words.add(Character.toUpperCase(word.charAt(0)) + word.substring(1)); + return String.join(" ", words); + } + + /** The English name of a part of the material named {@code display}. */ + static String label(String part, String display) { + return switch (part) { + case "block" -> "Block of " + display; + case "smallpowder" -> display + " Small Dust"; + case "smallblend" -> display + " Small Blend"; + case "powder_dirty" -> "Dirty " + display + " Dust"; + case "netherore" -> "Nether " + display + " Ore"; + case "endore" -> "End " + display + " Ore"; + case "weighted_pressure_plate" -> "Weighted " + display + " Pressure Plate"; + default -> display + " " + display(part.substring(part.indexOf(':') + 1)); + }; + } + + private static void itemTags( + String part, String name, String id, Map> tags) { + switch (part) { + case "bow" -> tag(tags, "item", "minecraft:enchantable/bow", id); + case "crossbow" -> tag(tags, "item", "minecraft:enchantable/crossbow", id); + case "fishing_rod" -> tag(tags, "item", "minecraft:enchantable/fishing", id); + default -> {} + } + if (Set.of( + "bow", "crossbow", "fishing_rod", "shears", "shield", "axe", "pickaxe", "shovel", "hoe", + "sword", "crackhammer", "scythe", "helmet", "chestplate", "leggings", "boots") + .contains(part)) tag(tags, "item", "minecraft:enchantable/durability", id); + if (Set.of("axe", "pickaxe", "shovel", "hoe", "crackhammer", "scythe").contains(part)) { + tag(tags, "item", "minecraft:enchantable/mining", id); + tag(tags, "item", "minecraft:enchantable/mining_loot", id); + } + if (Set.of("sword", "axe", "scythe", "crackhammer").contains(part)) { + tag(tags, "item", "minecraft:enchantable/weapon", id); + tag(tags, "item", "minecraft:enchantable/sharp_weapon", id); + } + if (part.equals("sword")) tag(tags, "item", "minecraft:enchantable/sword", id); + if (part.equals("arrow") || part.equals("bolt")) { + tag(tags, "item", "minecraft:arrows", id); + tag(tags, "item", "c:arrows", id); + } + if (Set.of("pickaxe", "axe", "shovel", "hoe", "sword").contains(part)) + tag(tags, "item", "minecraft:" + part + "s", id); + if (ARMOR.contains(part)) { + tag(tags, "item", "minecraft:enchantable/armor", id); + tag( + tags, + "item", + "minecraft:enchantable/" + + switch (part) { + case "helmet" -> "head_armor"; + case "chestplate" -> "chest_armor"; + case "leggings" -> "leg_armor"; + default -> "foot_armor"; + }, + id); + } + var common = + switch (part) { + case "dust" -> "dusts"; + case "ingot" -> "ingots"; + case "nugget" -> "nuggets"; + case "rod" -> "rods"; + case "gear" -> "gears"; + default -> null; + }; + if (common != null) { + tag(tags, "item", "c:" + common + "/" + name, id); + tag(tags, "item", "c:" + common, "#c:" + common + "/" + name); + } + } + + /** + * A block's loot table, by its Java factory: slabs drop two when double, a double slab drops two + * {@code slab}s (nothing without one), doors drop from their lower half, anvils keep their wear. + */ + private static Optional lootTable( + ResourceLocation factory, String id, String slab, boolean hasSlab) { + var part = factory.getNamespace().equals(NS) ? factory.getPath() : ""; + var entry = new JsonObject(); + entry.addProperty("type", "minecraft:item"); + var functions = new JsonArray(); + switch (part) { + case "double_slab" -> { + // A double slab is two slabs; without a slab part there is nothing to drop. + if (!hasSlab) return Optional.empty(); + functions.add(setCount(null)); + } + case "slab" -> { + var condition = new JsonObject(); + condition.addProperty("condition", "minecraft:block_state_property"); + condition.addProperty("block", id); + var state = new JsonObject(); + state.addProperty("type", "double"); + condition.add("properties", state); + functions.add(setCount(condition)); + } + case "anvil" -> { + var copy = new JsonObject(); + copy.addProperty("function", "minecraft:copy_state"); + copy.addProperty("block", id); + var properties = new JsonArray(); + properties.add("damage"); + copy.add("properties", properties); + functions.add(copy); + } + default -> {} + } + entry.addProperty("name", part.equals("double_slab") ? slab : id); + if (!functions.isEmpty()) entry.add("functions", functions); + var conditions = new JsonArray(); + var survives = new JsonObject(); + survives.addProperty("condition", "minecraft:survives_explosion"); + conditions.add(survives); + if (part.equals("door")) { + var lower = new JsonObject(); + lower.addProperty("condition", "minecraft:block_state_property"); + lower.addProperty("block", id); + var half = new JsonObject(); + half.addProperty("half", "lower"); + lower.add("properties", half); + conditions.add(lower); + } + var pool = new JsonObject(); + pool.addProperty("rolls", 1); + var entries = new JsonArray(); + entries.add(entry); + pool.add("entries", entries); + pool.add("conditions", conditions); + var table = new JsonObject(); + table.addProperty("type", "minecraft:block"); + var pools = new JsonArray(); + pools.add(pool); + table.add("pools", pools); + return Optional.of(json(table)); + } + + private static JsonObject setCount(JsonObject condition) { + var function = new JsonObject(); + function.addProperty("function", "minecraft:set_count"); + function.addProperty("count", 2); + if (condition != null) { + var conditions = new JsonArray(); + conditions.add(condition); + function.add("conditions", conditions); + } + return function; + } + + private static void tag(Map> tags, String kind, String tag, String id) { + tags.computeIfAbsent(kind + "|" + tag, key -> new TreeSet<>()).add(id); + } + + /** + * Collects template references below {@code element}: models (under "model" or "parent") into + * {@code models} and textures (under "textures") into {@code textures}, both as template paths. + */ + private static void references( + JsonElement element, String key, Deque models, Set textures) { + if (element.isJsonObject()) + element.getAsJsonObject().entrySet().forEach(e -> references(e.getValue(), e.getKey(), models, textures)); + else if (element.isJsonArray()) + element.getAsJsonArray().forEach(child -> references(child, key, models, textures)); + else if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + var value = element.getAsString(); + if (!value.startsWith(NS + ":") || !value.contains(T)) return; + var path = value.substring(NS.length() + 1); + if ("model".equals(key) || "parent".equals(key)) models.add("models/" + path); + else if (!"fluid".equals(key)) textures.add(path); + } + } + + /** + * The template's grayscale pixels multiplied by {@code argb}, with the untinted layer drawn over + * them, as PNG bytes. + */ + static byte[] colorize(byte[] tint, Optional untinted, int argb) { + try { + var base = ImageIO.read(new ByteArrayInputStream(tint)); + var over = untinted.isPresent() ? ImageIO.read(new ByteArrayInputStream(untinted.get())) : null; + int r = (argb >> 16) & 255, g = (argb >> 8) & 255, b = argb & 255; + var out = new BufferedImage(base.getWidth(), base.getHeight(), BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < base.getHeight(); y++) + for (int x = 0; x < base.getWidth(); x++) { + int p = base.getRGB(x, y); + int gray = (p >> 16) & 255; + int pixel = + (p & 0xFF000000) | (gray * r / 255) << 16 | (gray * g / 255) << 8 | gray * b / 255; + if (over != null) pixel = over(over.getRGB(x, y), pixel); + out.setRGB(x, y, pixel); + } + var bytes = new ByteArrayOutputStream(); + ImageIO.write(out, "png", bytes); + return bytes.toByteArray(); + } catch (IOException ex) { + throw new UncheckedIOException("Cannot color a material template", ex); + } + } + + /** Source-over alpha compositing of two ARGB pixels. */ + private static int over(int top, int bottom) { + int ta = top >>> 24; + if (ta == 255) return top; + if (ta == 0) return bottom; + int ba = bottom >>> 24; + int a = ta + ba * (255 - ta) / 255; + int[] result = new int[3]; + for (int shift = 16, i = 0; shift >= 0; shift -= 8, i++) { + int tc = (top >> shift) & 255, bc = (bottom >> shift) & 255; + result[i] = a == 0 ? 0 : (tc * ta + bc * ba * (255 - ta) / 255) / a; + } + return a << 24 | result[0] << 16 | result[1] << 8 | result[2]; + } + + private static String json(Map values) { + var object = new JsonObject(); + values.forEach(object::addProperty); + return json(object); + } + + private static String json(JsonElement element) { + return new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(element) + + "\n"; + } +} diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml index 0968d0a4..2fcb1f7d 100644 --- a/src/main/resources/META-INF/neoforge.mods.toml +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -80,3 +80,11 @@ ${mod_description} versionRange="${jei_version_range}" ordering="NONE" side="CLIENT" + +# Optional: CraftTweaker scripting integration when installed; never required. +[[dependencies.${mod_id}]] + modId="crafttweaker" + type="optional" + versionRange="${crafttweaker_version_range}" + ordering="NONE" + side="BOTH" diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___anvil.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___anvil.json new file mode 100644 index 00000000..c5cfb5a8 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___anvil.json @@ -0,0 +1,49 @@ +{ + "variants": { + "damage=0,facing=east": { + "model": "mmdlib:block/__material___anvil", + "y": 270 + }, + "damage=1,facing=east": { + "model": "mmdlib:block/__material___anvil_chipped", + "y": 270 + }, + "damage=2,facing=east": { + "model": "mmdlib:block/__material___anvil_damaged", + "y": 270 + }, + "damage=0,facing=north": { + "model": "mmdlib:block/__material___anvil", + "y": 180 + }, + "damage=1,facing=north": { + "model": "mmdlib:block/__material___anvil_chipped", + "y": 180 + }, + "damage=2,facing=north": { + "model": "mmdlib:block/__material___anvil_damaged", + "y": 180 + }, + "damage=0,facing=south": { + "model": "mmdlib:block/__material___anvil" + }, + "damage=1,facing=south": { + "model": "mmdlib:block/__material___anvil_chipped" + }, + "damage=2,facing=south": { + "model": "mmdlib:block/__material___anvil_damaged" + }, + "damage=0,facing=west": { + "model": "mmdlib:block/__material___anvil", + "y": 90 + }, + "damage=1,facing=west": { + "model": "mmdlib:block/__material___anvil_chipped", + "y": 90 + }, + "damage=2,facing=west": { + "model": "mmdlib:block/__material___anvil_damaged", + "y": 90 + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___bars.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___bars.json new file mode 100644 index 00000000..63209415 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___bars.json @@ -0,0 +1,100 @@ +{ + "multipart": [ + { + "apply": { + "model": "mmdlib:block/__material___bars_post_ends" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___bars_post" + }, + "when": { + "east": "false", + "north": "false", + "south": "false", + "west": "false" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___bars_cap" + }, + "when": { + "east": "false", + "north": "true", + "south": "false", + "west": "false" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___bars_cap", + "y": 90 + }, + "when": { + "east": "true", + "north": "false", + "south": "false", + "west": "false" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___bars_cap_alt" + }, + "when": { + "east": "false", + "north": "false", + "south": "true", + "west": "false" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___bars_cap_alt", + "y": 90 + }, + "when": { + "east": "false", + "north": "false", + "south": "false", + "west": "true" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___bars_side" + }, + "when": { + "north": "true" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___bars_side", + "y": 90 + }, + "when": { + "east": "true" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___bars_side_alt" + }, + "when": { + "south": "true" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___bars_side_alt", + "y": 90 + }, + "when": { + "west": "true" + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___block.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___block.json new file mode 100644 index 00000000..31b0b494 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "mmdlib:block/__material___block" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___bookshelf.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___bookshelf.json new file mode 100644 index 00000000..c62b6f3d --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___bookshelf.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "mmdlib:block/__material___bookshelf" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___button.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___button.json new file mode 100644 index 00000000..79b681fd --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___button.json @@ -0,0 +1,118 @@ +{ + "variants": { + "face=ceiling,facing=east,powered=false": { + "model": "mmdlib:block/__material___button", + "x": 180, + "y": 270 + }, + "face=ceiling,facing=east,powered=true": { + "model": "mmdlib:block/__material___button_pressed", + "x": 180, + "y": 270 + }, + "face=ceiling,facing=north,powered=false": { + "model": "mmdlib:block/__material___button", + "x": 180, + "y": 180 + }, + "face=ceiling,facing=north,powered=true": { + "model": "mmdlib:block/__material___button_pressed", + "x": 180, + "y": 180 + }, + "face=ceiling,facing=south,powered=false": { + "model": "mmdlib:block/__material___button", + "x": 180 + }, + "face=ceiling,facing=south,powered=true": { + "model": "mmdlib:block/__material___button_pressed", + "x": 180 + }, + "face=ceiling,facing=west,powered=false": { + "model": "mmdlib:block/__material___button", + "x": 180, + "y": 90 + }, + "face=ceiling,facing=west,powered=true": { + "model": "mmdlib:block/__material___button_pressed", + "x": 180, + "y": 90 + }, + "face=floor,facing=east,powered=false": { + "model": "mmdlib:block/__material___button", + "y": 90 + }, + "face=floor,facing=east,powered=true": { + "model": "mmdlib:block/__material___button_pressed", + "y": 90 + }, + "face=floor,facing=north,powered=false": { + "model": "mmdlib:block/__material___button" + }, + "face=floor,facing=north,powered=true": { + "model": "mmdlib:block/__material___button_pressed" + }, + "face=floor,facing=south,powered=false": { + "model": "mmdlib:block/__material___button", + "y": 180 + }, + "face=floor,facing=south,powered=true": { + "model": "mmdlib:block/__material___button_pressed", + "y": 180 + }, + "face=floor,facing=west,powered=false": { + "model": "mmdlib:block/__material___button", + "y": 270 + }, + "face=floor,facing=west,powered=true": { + "model": "mmdlib:block/__material___button_pressed", + "y": 270 + }, + "face=wall,facing=east,powered=false": { + "model": "mmdlib:block/__material___button", + "uvlock": true, + "x": 90, + "y": 90 + }, + "face=wall,facing=east,powered=true": { + "model": "mmdlib:block/__material___button_pressed", + "uvlock": true, + "x": 90, + "y": 90 + }, + "face=wall,facing=north,powered=false": { + "model": "mmdlib:block/__material___button", + "uvlock": true, + "x": 90 + }, + "face=wall,facing=north,powered=true": { + "model": "mmdlib:block/__material___button_pressed", + "uvlock": true, + "x": 90 + }, + "face=wall,facing=south,powered=false": { + "model": "mmdlib:block/__material___button", + "uvlock": true, + "x": 90, + "y": 180 + }, + "face=wall,facing=south,powered=true": { + "model": "mmdlib:block/__material___button_pressed", + "uvlock": true, + "x": 90, + "y": 180 + }, + "face=wall,facing=west,powered=false": { + "model": "mmdlib:block/__material___button", + "uvlock": true, + "x": 90, + "y": 270 + }, + "face=wall,facing=west,powered=true": { + "model": "mmdlib:block/__material___button_pressed", + "uvlock": true, + "x": 90, + "y": 270 + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___door.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___door.json new file mode 100644 index 00000000..8129e3be --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___door.json @@ -0,0 +1,124 @@ +{ + "variants": { + "facing=east,half=lower,hinge=left,open=false": { + "model": "mmdlib:block/__material___door_bottom_left" + }, + "facing=east,half=lower,hinge=left,open=true": { + "model": "mmdlib:block/__material___door_bottom_left_open", + "y": 90 + }, + "facing=east,half=lower,hinge=right,open=false": { + "model": "mmdlib:block/__material___door_bottom_right" + }, + "facing=east,half=lower,hinge=right,open=true": { + "model": "mmdlib:block/__material___door_bottom_right_open", + "y": 270 + }, + "facing=east,half=upper,hinge=left,open=false": { + "model": "mmdlib:block/__material___door_top_left" + }, + "facing=east,half=upper,hinge=left,open=true": { + "model": "mmdlib:block/__material___door_top_left_open", + "y": 90 + }, + "facing=east,half=upper,hinge=right,open=false": { + "model": "mmdlib:block/__material___door_top_right" + }, + "facing=east,half=upper,hinge=right,open=true": { + "model": "mmdlib:block/__material___door_top_right_open", + "y": 270 + }, + "facing=north,half=lower,hinge=left,open=false": { + "model": "mmdlib:block/__material___door_bottom_left", + "y": 270 + }, + "facing=north,half=lower,hinge=left,open=true": { + "model": "mmdlib:block/__material___door_bottom_left_open" + }, + "facing=north,half=lower,hinge=right,open=false": { + "model": "mmdlib:block/__material___door_bottom_right", + "y": 270 + }, + "facing=north,half=lower,hinge=right,open=true": { + "model": "mmdlib:block/__material___door_bottom_right_open", + "y": 180 + }, + "facing=north,half=upper,hinge=left,open=false": { + "model": "mmdlib:block/__material___door_top_left", + "y": 270 + }, + "facing=north,half=upper,hinge=left,open=true": { + "model": "mmdlib:block/__material___door_top_left_open" + }, + "facing=north,half=upper,hinge=right,open=false": { + "model": "mmdlib:block/__material___door_top_right", + "y": 270 + }, + "facing=north,half=upper,hinge=right,open=true": { + "model": "mmdlib:block/__material___door_top_right_open", + "y": 180 + }, + "facing=south,half=lower,hinge=left,open=false": { + "model": "mmdlib:block/__material___door_bottom_left", + "y": 90 + }, + "facing=south,half=lower,hinge=left,open=true": { + "model": "mmdlib:block/__material___door_bottom_left_open", + "y": 180 + }, + "facing=south,half=lower,hinge=right,open=false": { + "model": "mmdlib:block/__material___door_bottom_right", + "y": 90 + }, + "facing=south,half=lower,hinge=right,open=true": { + "model": "mmdlib:block/__material___door_bottom_right_open" + }, + "facing=south,half=upper,hinge=left,open=false": { + "model": "mmdlib:block/__material___door_top_left", + "y": 90 + }, + "facing=south,half=upper,hinge=left,open=true": { + "model": "mmdlib:block/__material___door_top_left_open", + "y": 180 + }, + "facing=south,half=upper,hinge=right,open=false": { + "model": "mmdlib:block/__material___door_top_right", + "y": 90 + }, + "facing=south,half=upper,hinge=right,open=true": { + "model": "mmdlib:block/__material___door_top_right_open" + }, + "facing=west,half=lower,hinge=left,open=false": { + "model": "mmdlib:block/__material___door_bottom_left", + "y": 180 + }, + "facing=west,half=lower,hinge=left,open=true": { + "model": "mmdlib:block/__material___door_bottom_left_open", + "y": 270 + }, + "facing=west,half=lower,hinge=right,open=false": { + "model": "mmdlib:block/__material___door_bottom_right", + "y": 180 + }, + "facing=west,half=lower,hinge=right,open=true": { + "model": "mmdlib:block/__material___door_bottom_right_open", + "y": 90 + }, + "facing=west,half=upper,hinge=left,open=false": { + "model": "mmdlib:block/__material___door_top_left", + "y": 180 + }, + "facing=west,half=upper,hinge=left,open=true": { + "model": "mmdlib:block/__material___door_top_left_open", + "y": 270 + }, + "facing=west,half=upper,hinge=right,open=false": { + "model": "mmdlib:block/__material___door_top_right", + "y": 180 + }, + "facing=west,half=upper,hinge=right,open=true": { + "model": "mmdlib:block/__material___door_top_right_open", + "y": 90 + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___double_slab.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___double_slab.json new file mode 100644 index 00000000..dfc507f1 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___double_slab.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "mmdlib:block/__material___slab_double" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___endore.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___endore.json new file mode 100644 index 00000000..3deaaff3 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___endore.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "mmdlib:block/__material___endore" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___fence.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___fence.json new file mode 100644 index 00000000..e8d3dc9a --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___fence.json @@ -0,0 +1,48 @@ +{ + "multipart": [ + { + "apply": { + "model": "mmdlib:block/__material___fence_post" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___fence_side", + "uvlock": true + }, + "when": { + "north": "true" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___fence_side", + "uvlock": true, + "y": 90 + }, + "when": { + "east": "true" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___fence_side", + "uvlock": true, + "y": 180 + }, + "when": { + "south": "true" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___fence_side", + "uvlock": true, + "y": 270 + }, + "when": { + "west": "true" + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___fence_gate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___fence_gate.json new file mode 100644 index 00000000..70181023 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___fence_gate.json @@ -0,0 +1,80 @@ +{ + "variants": { + "facing=east,in_wall=false,open=false": { + "model": "mmdlib:block/__material___fence_gate", + "uvlock": true, + "y": 270 + }, + "facing=east,in_wall=false,open=true": { + "model": "mmdlib:block/__material___fence_gate_open", + "uvlock": true, + "y": 270 + }, + "facing=east,in_wall=true,open=false": { + "model": "mmdlib:block/__material___fence_gate_wall", + "uvlock": true, + "y": 270 + }, + "facing=east,in_wall=true,open=true": { + "model": "mmdlib:block/__material___fence_gate_wall_open", + "uvlock": true, + "y": 270 + }, + "facing=north,in_wall=false,open=false": { + "model": "mmdlib:block/__material___fence_gate", + "uvlock": true, + "y": 180 + }, + "facing=north,in_wall=false,open=true": { + "model": "mmdlib:block/__material___fence_gate_open", + "uvlock": true, + "y": 180 + }, + "facing=north,in_wall=true,open=false": { + "model": "mmdlib:block/__material___fence_gate_wall", + "uvlock": true, + "y": 180 + }, + "facing=north,in_wall=true,open=true": { + "model": "mmdlib:block/__material___fence_gate_wall_open", + "uvlock": true, + "y": 180 + }, + "facing=south,in_wall=false,open=false": { + "model": "mmdlib:block/__material___fence_gate", + "uvlock": true + }, + "facing=south,in_wall=false,open=true": { + "model": "mmdlib:block/__material___fence_gate_open", + "uvlock": true + }, + "facing=south,in_wall=true,open=false": { + "model": "mmdlib:block/__material___fence_gate_wall", + "uvlock": true + }, + "facing=south,in_wall=true,open=true": { + "model": "mmdlib:block/__material___fence_gate_wall_open", + "uvlock": true + }, + "facing=west,in_wall=false,open=false": { + "model": "mmdlib:block/__material___fence_gate", + "uvlock": true, + "y": 90 + }, + "facing=west,in_wall=false,open=true": { + "model": "mmdlib:block/__material___fence_gate_open", + "uvlock": true, + "y": 90 + }, + "facing=west,in_wall=true,open=false": { + "model": "mmdlib:block/__material___fence_gate_wall", + "uvlock": true, + "y": 90 + }, + "facing=west,in_wall=true,open=true": { + "model": "mmdlib:block/__material___fence_gate_wall_open", + "uvlock": true, + "y": 90 + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___flower_pot.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___flower_pot.json new file mode 100644 index 00000000..157933fc --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___flower_pot.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "mmdlib:block/__material___flower_pot" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___ladder.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___ladder.json new file mode 100644 index 00000000..2cd67e02 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___ladder.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "mmdlib:block/__material___ladder", + "y": 90 + }, + "facing=north": { + "model": "mmdlib:block/__material___ladder" + }, + "facing=south": { + "model": "mmdlib:block/__material___ladder", + "y": 180 + }, + "facing=west": { + "model": "mmdlib:block/__material___ladder", + "y": 270 + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___lever.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___lever.json new file mode 100644 index 00000000..4dceb5aa --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___lever.json @@ -0,0 +1,110 @@ +{ + "variants": { + "face=ceiling,facing=east,powered=false": { + "model": "mmdlib:block/__material___lever_on", + "x": 180, + "y": 270 + }, + "face=ceiling,facing=east,powered=true": { + "model": "mmdlib:block/__material___lever", + "x": 180, + "y": 270 + }, + "face=ceiling,facing=north,powered=false": { + "model": "mmdlib:block/__material___lever_on", + "x": 180, + "y": 180 + }, + "face=ceiling,facing=north,powered=true": { + "model": "mmdlib:block/__material___lever", + "x": 180, + "y": 180 + }, + "face=ceiling,facing=south,powered=false": { + "model": "mmdlib:block/__material___lever_on", + "x": 180 + }, + "face=ceiling,facing=south,powered=true": { + "model": "mmdlib:block/__material___lever", + "x": 180 + }, + "face=ceiling,facing=west,powered=false": { + "model": "mmdlib:block/__material___lever_on", + "x": 180, + "y": 90 + }, + "face=ceiling,facing=west,powered=true": { + "model": "mmdlib:block/__material___lever", + "x": 180, + "y": 90 + }, + "face=floor,facing=east,powered=false": { + "model": "mmdlib:block/__material___lever_on", + "y": 90 + }, + "face=floor,facing=east,powered=true": { + "model": "mmdlib:block/__material___lever", + "y": 90 + }, + "face=floor,facing=north,powered=false": { + "model": "mmdlib:block/__material___lever_on" + }, + "face=floor,facing=north,powered=true": { + "model": "mmdlib:block/__material___lever" + }, + "face=floor,facing=south,powered=false": { + "model": "mmdlib:block/__material___lever_on", + "y": 180 + }, + "face=floor,facing=south,powered=true": { + "model": "mmdlib:block/__material___lever", + "y": 180 + }, + "face=floor,facing=west,powered=false": { + "model": "mmdlib:block/__material___lever_on", + "y": 270 + }, + "face=floor,facing=west,powered=true": { + "model": "mmdlib:block/__material___lever", + "y": 270 + }, + "face=wall,facing=east,powered=false": { + "model": "mmdlib:block/__material___lever_on", + "x": 90, + "y": 90 + }, + "face=wall,facing=east,powered=true": { + "model": "mmdlib:block/__material___lever", + "x": 90, + "y": 90 + }, + "face=wall,facing=north,powered=false": { + "model": "mmdlib:block/__material___lever_on", + "x": 90 + }, + "face=wall,facing=north,powered=true": { + "model": "mmdlib:block/__material___lever", + "x": 90 + }, + "face=wall,facing=south,powered=false": { + "model": "mmdlib:block/__material___lever_on", + "x": 90, + "y": 180 + }, + "face=wall,facing=south,powered=true": { + "model": "mmdlib:block/__material___lever", + "x": 90, + "y": 180 + }, + "face=wall,facing=west,powered=false": { + "model": "mmdlib:block/__material___lever_on", + "x": 90, + "y": 270 + }, + "face=wall,facing=west,powered=true": { + "model": "mmdlib:block/__material___lever", + "x": 90, + "y": 270 + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___molten.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___molten.json new file mode 100644 index 00000000..f17f93e2 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___molten.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "minecraft:block/air" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___netherore.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___netherore.json new file mode 100644 index 00000000..076c90b7 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___netherore.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "mmdlib:block/__material___netherore" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___ore.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___ore.json new file mode 100644 index 00000000..74ebe79e --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___ore.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "mmdlib:block/__material___ore" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___plate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___plate.json new file mode 100644 index 00000000..fe4cd8f7 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___plate.json @@ -0,0 +1,22 @@ +{ + "variants": { + "facing=down": { + "model": "mmdlib:block/__material___plate_down" + }, + "facing=up": { + "model": "mmdlib:block/__material___plate_up" + }, + "facing=north": { + "model": "mmdlib:block/__material___plate_north" + }, + "facing=south": { + "model": "mmdlib:block/__material___plate_south" + }, + "facing=west": { + "model": "mmdlib:block/__material___plate_west" + }, + "facing=east": { + "model": "mmdlib:block/__material___plate_east" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___pressure_plate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___pressure_plate.json new file mode 100644 index 00000000..146741f6 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___pressure_plate.json @@ -0,0 +1,10 @@ +{ + "variants": { + "powered=false": { + "model": "mmdlib:block/__material___pressure_plate" + }, + "powered=true": { + "model": "mmdlib:block/__material___pressure_plate_down" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___slab.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___slab.json new file mode 100644 index 00000000..c5a0be1c --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___slab.json @@ -0,0 +1,13 @@ +{ + "variants": { + "type=bottom": { + "model": "mmdlib:block/__material___slab" + }, + "type=double": { + "model": "mmdlib:block/__material___slab_double" + }, + "type=top": { + "model": "mmdlib:block/__material___slab_top" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___stairs.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___stairs.json new file mode 100644 index 00000000..50742a2c --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___stairs.json @@ -0,0 +1,209 @@ +{ + "variants": { + "facing=east,half=bottom,shape=inner_left": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "y": 270 + }, + "facing=east,half=bottom,shape=inner_right": { + "model": "mmdlib:block/__material___stairs_inner" + }, + "facing=east,half=bottom,shape=outer_left": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "y": 270 + }, + "facing=east,half=bottom,shape=outer_right": { + "model": "mmdlib:block/__material___stairs_outer" + }, + "facing=east,half=bottom,shape=straight": { + "model": "mmdlib:block/__material___stairs" + }, + "facing=east,half=top,shape=inner_left": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "x": 180 + }, + "facing=east,half=top,shape=inner_right": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "x": 180, + "y": 90 + }, + "facing=east,half=top,shape=outer_left": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "x": 180 + }, + "facing=east,half=top,shape=outer_right": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "x": 180, + "y": 90 + }, + "facing=east,half=top,shape=straight": { + "model": "mmdlib:block/__material___stairs", + "uvlock": true, + "x": 180 + }, + "facing=north,half=bottom,shape=inner_left": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "y": 180 + }, + "facing=north,half=bottom,shape=inner_right": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "y": 270 + }, + "facing=north,half=bottom,shape=outer_left": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "y": 180 + }, + "facing=north,half=bottom,shape=outer_right": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "y": 270 + }, + "facing=north,half=bottom,shape=straight": { + "model": "mmdlib:block/__material___stairs", + "uvlock": true, + "y": 270 + }, + "facing=north,half=top,shape=inner_left": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "x": 180, + "y": 270 + }, + "facing=north,half=top,shape=inner_right": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "x": 180 + }, + "facing=north,half=top,shape=outer_left": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "x": 180, + "y": 270 + }, + "facing=north,half=top,shape=outer_right": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "x": 180 + }, + "facing=north,half=top,shape=straight": { + "model": "mmdlib:block/__material___stairs", + "uvlock": true, + "x": 180, + "y": 270 + }, + "facing=south,half=bottom,shape=inner_left": { + "model": "mmdlib:block/__material___stairs_inner" + }, + "facing=south,half=bottom,shape=inner_right": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "y": 90 + }, + "facing=south,half=bottom,shape=outer_left": { + "model": "mmdlib:block/__material___stairs_outer" + }, + "facing=south,half=bottom,shape=outer_right": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "y": 90 + }, + "facing=south,half=bottom,shape=straight": { + "model": "mmdlib:block/__material___stairs", + "uvlock": true, + "y": 90 + }, + "facing=south,half=top,shape=inner_left": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "x": 180, + "y": 90 + }, + "facing=south,half=top,shape=inner_right": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "x": 180, + "y": 180 + }, + "facing=south,half=top,shape=outer_left": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "x": 180, + "y": 90 + }, + "facing=south,half=top,shape=outer_right": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "x": 180, + "y": 180 + }, + "facing=south,half=top,shape=straight": { + "model": "mmdlib:block/__material___stairs", + "uvlock": true, + "x": 180, + "y": 90 + }, + "facing=west,half=bottom,shape=inner_left": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "y": 90 + }, + "facing=west,half=bottom,shape=inner_right": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "y": 180 + }, + "facing=west,half=bottom,shape=outer_left": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "y": 90 + }, + "facing=west,half=bottom,shape=outer_right": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "y": 180 + }, + "facing=west,half=bottom,shape=straight": { + "model": "mmdlib:block/__material___stairs", + "uvlock": true, + "y": 180 + }, + "facing=west,half=top,shape=inner_left": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "x": 180, + "y": 180 + }, + "facing=west,half=top,shape=inner_right": { + "model": "mmdlib:block/__material___stairs_inner", + "uvlock": true, + "x": 180, + "y": 270 + }, + "facing=west,half=top,shape=outer_left": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "x": 180, + "y": 180 + }, + "facing=west,half=top,shape=outer_right": { + "model": "mmdlib:block/__material___stairs_outer", + "uvlock": true, + "x": 180, + "y": 270 + }, + "facing=west,half=top,shape=straight": { + "model": "mmdlib:block/__material___stairs", + "uvlock": true, + "x": 180, + "y": 180 + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___trapdoor.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___trapdoor.json new file mode 100644 index 00000000..950b64e4 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___trapdoor.json @@ -0,0 +1,58 @@ +{ + "variants": { + "facing=east,half=bottom,open=false": { + "model": "mmdlib:block/__material___trapdoor_bottom" + }, + "facing=east,half=bottom,open=true": { + "model": "mmdlib:block/__material___trapdoor_open", + "y": 90 + }, + "facing=east,half=top,open=false": { + "model": "mmdlib:block/__material___trapdoor_top" + }, + "facing=east,half=top,open=true": { + "model": "mmdlib:block/__material___trapdoor_open", + "y": 90 + }, + "facing=north,half=bottom,open=false": { + "model": "mmdlib:block/__material___trapdoor_bottom" + }, + "facing=north,half=bottom,open=true": { + "model": "mmdlib:block/__material___trapdoor_open" + }, + "facing=north,half=top,open=false": { + "model": "mmdlib:block/__material___trapdoor_top" + }, + "facing=north,half=top,open=true": { + "model": "mmdlib:block/__material___trapdoor_open" + }, + "facing=south,half=bottom,open=false": { + "model": "mmdlib:block/__material___trapdoor_bottom" + }, + "facing=south,half=bottom,open=true": { + "model": "mmdlib:block/__material___trapdoor_open", + "y": 180 + }, + "facing=south,half=top,open=false": { + "model": "mmdlib:block/__material___trapdoor_top" + }, + "facing=south,half=top,open=true": { + "model": "mmdlib:block/__material___trapdoor_open", + "y": 180 + }, + "facing=west,half=bottom,open=false": { + "model": "mmdlib:block/__material___trapdoor_bottom" + }, + "facing=west,half=bottom,open=true": { + "model": "mmdlib:block/__material___trapdoor_open", + "y": 270 + }, + "facing=west,half=top,open=false": { + "model": "mmdlib:block/__material___trapdoor_top" + }, + "facing=west,half=top,open=true": { + "model": "mmdlib:block/__material___trapdoor_open", + "y": 270 + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___tripwire_hook.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___tripwire_hook.json new file mode 100644 index 00000000..d4694216 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___tripwire_hook.json @@ -0,0 +1,64 @@ +{ + "variants": { + "attached=false,facing=east,powered=false": { + "model": "mmdlib:block/__material___tripwire_hook", + "y": 90 + }, + "attached=false,facing=east,powered=true": { + "model": "mmdlib:block/__material___tripwire_hook_on", + "y": 90 + }, + "attached=false,facing=north,powered=false": { + "model": "mmdlib:block/__material___tripwire_hook" + }, + "attached=false,facing=north,powered=true": { + "model": "mmdlib:block/__material___tripwire_hook_on" + }, + "attached=false,facing=south,powered=false": { + "model": "mmdlib:block/__material___tripwire_hook", + "y": 180 + }, + "attached=false,facing=south,powered=true": { + "model": "mmdlib:block/__material___tripwire_hook_on", + "y": 180 + }, + "attached=false,facing=west,powered=false": { + "model": "mmdlib:block/__material___tripwire_hook", + "y": 270 + }, + "attached=false,facing=west,powered=true": { + "model": "mmdlib:block/__material___tripwire_hook_on", + "y": 270 + }, + "attached=true,facing=east,powered=false": { + "model": "mmdlib:block/__material___tripwire_hook_attached", + "y": 90 + }, + "attached=true,facing=east,powered=true": { + "model": "mmdlib:block/__material___tripwire_hook_attached_on", + "y": 90 + }, + "attached=true,facing=north,powered=false": { + "model": "mmdlib:block/__material___tripwire_hook_attached" + }, + "attached=true,facing=north,powered=true": { + "model": "mmdlib:block/__material___tripwire_hook_attached_on" + }, + "attached=true,facing=south,powered=false": { + "model": "mmdlib:block/__material___tripwire_hook_attached", + "y": 180 + }, + "attached=true,facing=south,powered=true": { + "model": "mmdlib:block/__material___tripwire_hook_attached_on", + "y": 180 + }, + "attached=true,facing=west,powered=false": { + "model": "mmdlib:block/__material___tripwire_hook_attached", + "y": 270 + }, + "attached=true,facing=west,powered=true": { + "model": "mmdlib:block/__material___tripwire_hook_attached_on", + "y": 270 + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___wall.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___wall.json new file mode 100644 index 00000000..df1cb685 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___wall.json @@ -0,0 +1,90 @@ +{ + "multipart": [ + { + "apply": { + "model": "mmdlib:block/__material___wall_post" + }, + "when": { + "up": "true" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___wall_side", + "uvlock": true + }, + "when": { + "north": "low" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___wall_side", + "uvlock": true, + "y": 90 + }, + "when": { + "east": "low" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___wall_side", + "uvlock": true, + "y": 180 + }, + "when": { + "south": "low" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___wall_side", + "uvlock": true, + "y": 270 + }, + "when": { + "west": "low" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___wall_side_tall", + "uvlock": true + }, + "when": { + "north": "tall" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___wall_side_tall", + "uvlock": true, + "y": 90 + }, + "when": { + "east": "tall" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___wall_side_tall", + "uvlock": true, + "y": 180 + }, + "when": { + "south": "tall" + } + }, + { + "apply": { + "model": "mmdlib:block/__material___wall_side_tall", + "uvlock": true, + "y": 270 + }, + "when": { + "west": "tall" + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___weighted_pressure_plate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___weighted_pressure_plate.json new file mode 100644 index 00000000..7430392a --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___weighted_pressure_plate.json @@ -0,0 +1,52 @@ +{ + "variants": { + "power=0": { + "model": "mmdlib:block/__material___weighted_pressure_plate" + }, + "power=1": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=10": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=11": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=12": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=13": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=14": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=15": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=2": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=3": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=4": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=5": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=6": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=7": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=8": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + }, + "power=9": { + "model": "mmdlib:block/__material___weighted_pressure_plate_down" + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil.json new file mode 100644 index 00000000..47dd6a4f --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/anvil", + "textures": { + "particle": "mmdlib:block/__material___anvil_base", + "body": "mmdlib:block/__material___anvil_base", + "top": "mmdlib:block/__material___anvil_top_damaged_0" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil_chipped.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil_chipped.json new file mode 100644 index 00000000..42afd171 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil_chipped.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/chipped_anvil", + "textures": { + "particle": "mmdlib:block/__material___anvil_base", + "body": "mmdlib:block/__material___anvil_base", + "top": "mmdlib:block/__material___anvil_top_damaged_1" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil_damaged.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil_damaged.json new file mode 100644 index 00000000..211a43e7 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___anvil_damaged.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/damaged_anvil", + "textures": { + "particle": "mmdlib:block/__material___anvil_base", + "body": "mmdlib:block/__material___anvil_base", + "top": "mmdlib:block/__material___anvil_top_damaged_2" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_cap.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_cap.json new file mode 100644 index 00000000..83858787 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_cap.json @@ -0,0 +1,9 @@ +{ + "parent": "minecraft:block/iron_bars_cap", + "textures": { + "particle": "mmdlib:block/__material___bars", + "bars": "mmdlib:block/__material___bars", + "edge": "mmdlib:block/__material___bars" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_cap_alt.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_cap_alt.json new file mode 100644 index 00000000..f99bb917 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_cap_alt.json @@ -0,0 +1,9 @@ +{ + "parent": "minecraft:block/iron_bars_cap_alt", + "textures": { + "particle": "mmdlib:block/__material___bars", + "bars": "mmdlib:block/__material___bars", + "edge": "mmdlib:block/__material___bars" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_post.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_post.json new file mode 100644 index 00000000..e1d5b7c9 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_post.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/iron_bars_post", + "textures": { + "particle": "mmdlib:block/__material___bars", + "bars": "mmdlib:block/__material___bars" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_post_ends.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_post_ends.json new file mode 100644 index 00000000..af1028cd --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_post_ends.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/iron_bars_post_ends", + "textures": { + "particle": "mmdlib:block/__material___bars", + "edge": "mmdlib:block/__material___bars" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_side.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_side.json new file mode 100644 index 00000000..0dac7c96 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_side.json @@ -0,0 +1,9 @@ +{ + "parent": "minecraft:block/iron_bars_side", + "textures": { + "particle": "mmdlib:block/__material___bars", + "bars": "mmdlib:block/__material___bars", + "edge": "mmdlib:block/__material___bars" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_side_alt.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_side_alt.json new file mode 100644 index 00000000..fcf77806 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bars_side_alt.json @@ -0,0 +1,9 @@ +{ + "parent": "minecraft:block/iron_bars_side_alt", + "textures": { + "particle": "mmdlib:block/__material___bars", + "bars": "mmdlib:block/__material___bars", + "edge": "mmdlib:block/__material___bars" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___block.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___block.json new file mode 100644 index 00000000..04eee305 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___block.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/iron_block", + "textures": { + "all": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bookshelf.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bookshelf.json new file mode 100644 index 00000000..cc4710fa --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___bookshelf.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/bookshelf", + "textures": { + "end": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button.json new file mode 100644 index 00000000..c4123b34 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/stone_button", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button_inventory.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button_inventory.json new file mode 100644 index 00000000..6e89adf0 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button_inventory.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/stone_button_inventory", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button_pressed.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button_pressed.json new file mode 100644 index 00000000..c1fd32cb --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___button_pressed.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/stone_button_pressed", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_left.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_left.json new file mode 100644 index 00000000..446e8db8 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_left.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/iron_door_bottom_left", + "textures": { + "bottom": "mmdlib:block/__material___door_lower", + "top": "mmdlib:block/__material___door_upper" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_left_open.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_left_open.json new file mode 100644 index 00000000..cd400a31 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_left_open.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/iron_door_bottom_left_open", + "textures": { + "bottom": "mmdlib:block/__material___door_lower", + "top": "mmdlib:block/__material___door_upper" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_right.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_right.json new file mode 100644 index 00000000..6e3bfb8f --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_right.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/iron_door_bottom_right", + "textures": { + "bottom": "mmdlib:block/__material___door_lower", + "top": "mmdlib:block/__material___door_upper" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_right_open.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_right_open.json new file mode 100644 index 00000000..b2887db0 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_bottom_right_open.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/iron_door_bottom_right_open", + "textures": { + "bottom": "mmdlib:block/__material___door_lower", + "top": "mmdlib:block/__material___door_upper" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_left.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_left.json new file mode 100644 index 00000000..2fc75571 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_left.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/iron_door_top_left", + "textures": { + "bottom": "mmdlib:block/__material___door_lower", + "top": "mmdlib:block/__material___door_upper" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_left_open.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_left_open.json new file mode 100644 index 00000000..9ef58873 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_left_open.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/iron_door_top_left_open", + "textures": { + "bottom": "mmdlib:block/__material___door_lower", + "top": "mmdlib:block/__material___door_upper" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_right.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_right.json new file mode 100644 index 00000000..8448e712 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_right.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/iron_door_top_right", + "textures": { + "bottom": "mmdlib:block/__material___door_lower", + "top": "mmdlib:block/__material___door_upper" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_right_open.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_right_open.json new file mode 100644 index 00000000..96f2026d --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___door_top_right_open.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/iron_door_top_right_open", + "textures": { + "bottom": "mmdlib:block/__material___door_lower", + "top": "mmdlib:block/__material___door_upper" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___endore.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___endore.json new file mode 100644 index 00000000..53295b26 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___endore.json @@ -0,0 +1,87 @@ +{ + "parent": "minecraft:block/block", + "render_type": "minecraft:cutout", + "textures": { + "particle": "minecraft:block/end_stone", + "base": "minecraft:block/end_stone", + "overlay": "mmdlib:block/__material___ore_overlay" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "up": { + "texture": "#base", + "cullface": "up" + }, + "down": { + "texture": "#base", + "cullface": "down" + }, + "north": { + "texture": "#base", + "cullface": "north" + }, + "south": { + "texture": "#base", + "cullface": "south" + }, + "east": { + "texture": "#base", + "cullface": "east" + }, + "west": { + "texture": "#base", + "cullface": "west" + } + } + }, + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "up": { + "texture": "#overlay", + "cullface": "up" + }, + "down": { + "texture": "#overlay", + "cullface": "down" + }, + "north": { + "texture": "#overlay", + "cullface": "north" + }, + "south": { + "texture": "#overlay", + "cullface": "south" + }, + "east": { + "texture": "#overlay", + "cullface": "east" + }, + "west": { + "texture": "#overlay", + "cullface": "west" + } + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate.json new file mode 100644 index 00000000..2ec61e33 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/oak_fence_gate", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_open.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_open.json new file mode 100644 index 00000000..00352736 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_open.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/oak_fence_gate_open", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_wall.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_wall.json new file mode 100644 index 00000000..1020ea1c --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_wall.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/oak_fence_gate_wall", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_wall_open.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_wall_open.json new file mode 100644 index 00000000..95c43814 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_gate_wall_open.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/oak_fence_gate_wall_open", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_inventory.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_inventory.json new file mode 100644 index 00000000..ff900484 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_inventory.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/oak_fence_inventory", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_post.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_post.json new file mode 100644 index 00000000..85556f07 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_post.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/oak_fence_post", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_side.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_side.json new file mode 100644 index 00000000..c4f844ca --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___fence_side.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/oak_fence_side", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___flower_pot.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___flower_pot.json new file mode 100644 index 00000000..c96a899b --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___flower_pot.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/flower_pot", + "textures": { + "particle": "mmdlib:block/__material___block", + "flowerpot": "mmdlib:block/__material___block" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___ladder.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___ladder.json new file mode 100644 index 00000000..0929df7f --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___ladder.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/ladder", + "textures": { + "particle": "mmdlib:block/__material___ladder", + "texture": "mmdlib:block/__material___ladder" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___lever.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___lever.json new file mode 100644 index 00000000..74a27b12 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___lever.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/lever", + "textures": { + "particle": "mmdlib:block/__material___block", + "base": "mmdlib:block/__material___block" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___lever_on.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___lever_on.json new file mode 100644 index 00000000..ae489738 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___lever_on.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/lever_on", + "textures": { + "particle": "mmdlib:block/__material___block", + "base": "mmdlib:block/__material___block" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___netherore.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___netherore.json new file mode 100644 index 00000000..85188172 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___netherore.json @@ -0,0 +1,87 @@ +{ + "parent": "minecraft:block/block", + "render_type": "minecraft:cutout", + "textures": { + "particle": "minecraft:block/netherrack", + "base": "minecraft:block/netherrack", + "overlay": "mmdlib:block/__material___ore_overlay" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "up": { + "texture": "#base", + "cullface": "up" + }, + "down": { + "texture": "#base", + "cullface": "down" + }, + "north": { + "texture": "#base", + "cullface": "north" + }, + "south": { + "texture": "#base", + "cullface": "south" + }, + "east": { + "texture": "#base", + "cullface": "east" + }, + "west": { + "texture": "#base", + "cullface": "west" + } + } + }, + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "up": { + "texture": "#overlay", + "cullface": "up" + }, + "down": { + "texture": "#overlay", + "cullface": "down" + }, + "north": { + "texture": "#overlay", + "cullface": "north" + }, + "south": { + "texture": "#overlay", + "cullface": "south" + }, + "east": { + "texture": "#overlay", + "cullface": "east" + }, + "west": { + "texture": "#overlay", + "cullface": "west" + } + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___ore.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___ore.json new file mode 100644 index 00000000..65e2b319 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___ore.json @@ -0,0 +1,87 @@ +{ + "parent": "minecraft:block/block", + "render_type": "minecraft:cutout", + "textures": { + "particle": "minecraft:block/stone", + "base": "minecraft:block/stone", + "overlay": "mmdlib:block/__material___ore_overlay" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "up": { + "texture": "#base", + "cullface": "up" + }, + "down": { + "texture": "#base", + "cullface": "down" + }, + "north": { + "texture": "#base", + "cullface": "north" + }, + "south": { + "texture": "#base", + "cullface": "south" + }, + "east": { + "texture": "#base", + "cullface": "east" + }, + "west": { + "texture": "#base", + "cullface": "west" + } + } + }, + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "up": { + "texture": "#overlay", + "cullface": "up" + }, + "down": { + "texture": "#overlay", + "cullface": "down" + }, + "north": { + "texture": "#overlay", + "cullface": "north" + }, + "south": { + "texture": "#overlay", + "cullface": "south" + }, + "east": { + "texture": "#overlay", + "cullface": "east" + }, + "west": { + "texture": "#overlay", + "cullface": "west" + } + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_down.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_down.json new file mode 100644 index 00000000..da9042b0 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_down.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "all": "mmdlib:block/__material___plate", + "particle": "mmdlib:block/__material___plate" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 1, + 16 + ], + "faces": { + "up": { + "texture": "#all" + }, + "down": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_east.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_east.json new file mode 100644 index 00000000..cc3342a4 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_east.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "all": "mmdlib:block/__material___plate", + "particle": "mmdlib:block/__material___plate" + }, + "elements": [ + { + "from": [ + 15, + 0, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "up": { + "texture": "#all" + }, + "down": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_north.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_north.json new file mode 100644 index 00000000..5d766444 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_north.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "all": "mmdlib:block/__material___plate", + "particle": "mmdlib:block/__material___plate" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 16, + 1 + ], + "faces": { + "up": { + "texture": "#all" + }, + "down": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_south.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_south.json new file mode 100644 index 00000000..130c3863 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_south.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "all": "mmdlib:block/__material___plate", + "particle": "mmdlib:block/__material___plate" + }, + "elements": [ + { + "from": [ + 0, + 0, + 15 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "up": { + "texture": "#all" + }, + "down": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_up.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_up.json new file mode 100644 index 00000000..b301f0e5 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_up.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "all": "mmdlib:block/__material___plate", + "particle": "mmdlib:block/__material___plate" + }, + "elements": [ + { + "from": [ + 0, + 15, + 0 + ], + "to": [ + 16, + 16, + 16 + ], + "faces": { + "up": { + "texture": "#all" + }, + "down": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_west.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_west.json new file mode 100644 index 00000000..c09e9af4 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___plate_west.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "all": "mmdlib:block/__material___plate", + "particle": "mmdlib:block/__material___plate" + }, + "elements": [ + { + "from": [ + 0, + 0, + 0 + ], + "to": [ + 1, + 16, + 16 + ], + "faces": { + "up": { + "texture": "#all" + }, + "down": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + } + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___pressure_plate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___pressure_plate.json new file mode 100644 index 00000000..3bbcae3b --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___pressure_plate.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/stone_pressure_plate", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___pressure_plate_down.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___pressure_plate_down.json new file mode 100644 index 00000000..9a834104 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___pressure_plate_down.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/stone_pressure_plate_down", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab.json new file mode 100644 index 00000000..b4e14135 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/cut_copper_slab", + "textures": { + "bottom": "mmdlib:block/__material___block", + "side": "mmdlib:block/__material___block", + "top": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab_double.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab_double.json new file mode 100644 index 00000000..e82b8fb9 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab_double.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cut_copper", + "textures": { + "all": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab_top.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab_top.json new file mode 100644 index 00000000..5585d8b8 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___slab_top.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/cut_copper_slab_top", + "textures": { + "bottom": "mmdlib:block/__material___block", + "side": "mmdlib:block/__material___block", + "top": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs.json new file mode 100644 index 00000000..3e02e970 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/cut_copper_stairs", + "textures": { + "bottom": "mmdlib:block/__material___block", + "side": "mmdlib:block/__material___block", + "top": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs_inner.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs_inner.json new file mode 100644 index 00000000..661dd7ba --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs_inner.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/cut_copper_stairs_inner", + "textures": { + "bottom": "mmdlib:block/__material___block", + "side": "mmdlib:block/__material___block", + "top": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs_outer.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs_outer.json new file mode 100644 index 00000000..a18195e4 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___stairs_outer.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/cut_copper_stairs_outer", + "textures": { + "bottom": "mmdlib:block/__material___block", + "side": "mmdlib:block/__material___block", + "top": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_bottom.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_bottom.json new file mode 100644 index 00000000..0847b5d3 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_bottom.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:block/iron_trapdoor_bottom", + "textures": { + "texture": "mmdlib:block/__material___trapdoor" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_open.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_open.json new file mode 100644 index 00000000..ca250814 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_open.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:block/iron_trapdoor_open", + "textures": { + "texture": "mmdlib:block/__material___trapdoor" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_top.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_top.json new file mode 100644 index 00000000..4c9ed563 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___trapdoor_top.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:block/iron_trapdoor_top", + "textures": { + "texture": "mmdlib:block/__material___trapdoor" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook.json new file mode 100644 index 00000000..c5089d1a --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/tripwire_hook", + "textures": { + "particle": "mmdlib:block/__material___block", + "wood": "mmdlib:block/__material___block" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_attached.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_attached.json new file mode 100644 index 00000000..361a23ab --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_attached.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/tripwire_hook_attached", + "textures": { + "particle": "mmdlib:block/__material___block", + "wood": "mmdlib:block/__material___block" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_attached_on.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_attached_on.json new file mode 100644 index 00000000..2c56a2db --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_attached_on.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/tripwire_hook_attached_on", + "textures": { + "particle": "mmdlib:block/__material___block", + "wood": "mmdlib:block/__material___block" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_on.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_on.json new file mode 100644 index 00000000..49718cba --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___tripwire_hook_on.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/tripwire_hook_on", + "textures": { + "particle": "mmdlib:block/__material___block", + "wood": "mmdlib:block/__material___block" + }, + "render_type": "minecraft:cutout" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_inventory.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_inventory.json new file mode 100644 index 00000000..c5c4003b --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_inventory.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cobblestone_wall_inventory", + "textures": { + "wall": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_post.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_post.json new file mode 100644 index 00000000..e6192fcf --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_post.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cobblestone_wall_post", + "textures": { + "wall": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_side.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_side.json new file mode 100644 index 00000000..a4ad20ce --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_side.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cobblestone_wall_side", + "textures": { + "wall": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_side_tall.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_side_tall.json new file mode 100644 index 00000000..07344cf4 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___wall_side_tall.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cobblestone_wall_side_tall", + "textures": { + "wall": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___weighted_pressure_plate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___weighted_pressure_plate.json new file mode 100644 index 00000000..94896d0c --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___weighted_pressure_plate.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/heavy_weighted_pressure_plate", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___weighted_pressure_plate_down.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___weighted_pressure_plate_down.json new file mode 100644 index 00000000..213be125 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___weighted_pressure_plate_down.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/heavy_weighted_pressure_plate_down", + "textures": { + "texture": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___anvil.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___anvil.json new file mode 100644 index 00000000..3650ccce --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___anvil.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___anvil" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___arrow.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___arrow.json new file mode 100644 index 00000000..72043c3f --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___arrow.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___arrow" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___axe.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___axe.json new file mode 100644 index 00000000..6a7111ed --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___axe.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "mmdlib:item/__material___axe" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bars.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bars.json new file mode 100644 index 00000000..efc1588e --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bars.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:block/__material___bars" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___blend.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___blend.json new file mode 100644 index 00000000..ec90db02 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___blend.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___blend" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___block.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___block.json new file mode 100644 index 00000000..3d023041 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___block.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___block" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bolt.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bolt.json new file mode 100644 index 00000000..7ce3c902 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bolt.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___bolt" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bookshelf.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bookshelf.json new file mode 100644 index 00000000..c085f7cc --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bookshelf.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___bookshelf" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___boots.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___boots.json new file mode 100644 index 00000000..b17193ae --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___boots.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___boots" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow.json new file mode 100644 index 00000000..f0ec28b8 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow.json @@ -0,0 +1,28 @@ +{ + "parent": "minecraft:item/bow", + "textures": { + "layer0": "mmdlib:item/__material___bow_standby" + }, + "overrides": [ + { + "predicate": { + "pulling": 1 + }, + "model": "mmdlib:item/__material___bow_pulling_0" + }, + { + "predicate": { + "pulling": 1, + "pull": 0.65 + }, + "model": "mmdlib:item/__material___bow_pulling_1" + }, + { + "predicate": { + "pulling": 1, + "pull": 0.9 + }, + "model": "mmdlib:item/__material___bow_pulling_2" + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_0.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_0.json new file mode 100644 index 00000000..251c58b3 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_0.json @@ -0,0 +1,6 @@ +{ + "parent": "mmdlib:item/__material___bow", + "textures": { + "layer0": "mmdlib:item/__material___bow_pulling_0" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_1.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_1.json new file mode 100644 index 00000000..271f9587 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_1.json @@ -0,0 +1,6 @@ +{ + "parent": "mmdlib:item/__material___bow", + "textures": { + "layer0": "mmdlib:item/__material___bow_pulling_1" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_2.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_2.json new file mode 100644 index 00000000..0aa788c0 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___bow_pulling_2.json @@ -0,0 +1,6 @@ +{ + "parent": "mmdlib:item/__material___bow", + "textures": { + "layer0": "mmdlib:item/__material___bow_pulling_2" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___button.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___button.json new file mode 100644 index 00000000..bf6ceb5a --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___button.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___button_inventory" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___chestplate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___chestplate.json new file mode 100644 index 00000000..5d20eb79 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___chestplate.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___chestplate" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___clump.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___clump.json new file mode 100644 index 00000000..d1e82a46 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___clump.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___clump" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crackhammer.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crackhammer.json new file mode 100644 index 00000000..593cf6df --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crackhammer.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "mmdlib:item/__material___crackhammer" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow.json new file mode 100644 index 00000000..7f758e5d --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow.json @@ -0,0 +1,41 @@ +{ + "parent": "minecraft:item/crossbow", + "textures": { + "layer0": "mmdlib:item/__material___crossbow_standby" + }, + "overrides": [ + { + "predicate": { + "pulling": 1 + }, + "model": "mmdlib:item/__material___crossbow_pulling_0" + }, + { + "predicate": { + "pulling": 1, + "pull": 0.65 + }, + "model": "mmdlib:item/__material___crossbow_pulling_1" + }, + { + "predicate": { + "pulling": 1, + "pull": 0.9 + }, + "model": "mmdlib:item/__material___crossbow_pulling_2" + }, + { + "predicate": { + "charged": 1 + }, + "model": "mmdlib:item/__material___crossbow_arrow" + }, + { + "predicate": { + "charged": 1, + "firework": 1 + }, + "model": "mmdlib:item/__material___crossbow_firework" + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_arrow.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_arrow.json new file mode 100644 index 00000000..cffa8764 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_arrow.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/crossbow", + "textures": { + "layer0": "mmdlib:item/__material___crossbow_pulling_2" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_firework.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_firework.json new file mode 100644 index 00000000..cffa8764 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_firework.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/crossbow", + "textures": { + "layer0": "mmdlib:item/__material___crossbow_pulling_2" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_0.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_0.json new file mode 100644 index 00000000..6466bad7 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_0.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/crossbow", + "textures": { + "layer0": "mmdlib:item/__material___crossbow_pulling_0" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_1.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_1.json new file mode 100644 index 00000000..21fbb75a --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_1.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/crossbow", + "textures": { + "layer0": "mmdlib:item/__material___crossbow_pulling_1" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_2.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_2.json new file mode 100644 index 00000000..cffa8764 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crossbow_pulling_2.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/crossbow", + "textures": { + "layer0": "mmdlib:item/__material___crossbow_pulling_2" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crystal.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crystal.json new file mode 100644 index 00000000..55fadd4f --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___crystal.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___crystal" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___door.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___door.json new file mode 100644 index 00000000..b7da7a45 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___door.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___door" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___dust.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___dust.json new file mode 100644 index 00000000..3e22523f --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___dust.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___dust" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___endore.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___endore.json new file mode 100644 index 00000000..c13981f6 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___endore.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___endore" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fence.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fence.json new file mode 100644 index 00000000..966cfd41 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fence.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___fence_inventory" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fence_gate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fence_gate.json new file mode 100644 index 00000000..fca9c657 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fence_gate.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___fence_gate" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fishing_rod.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fishing_rod.json new file mode 100644 index 00000000..aface0a1 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fishing_rod.json @@ -0,0 +1,14 @@ +{ + "parent": "minecraft:item/handheld_rod", + "textures": { + "layer0": "mmdlib:item/__material___fishing_rod_uncast" + }, + "overrides": [ + { + "predicate": { + "cast": 1 + }, + "model": "mmdlib:item/__material___fishing_rod_cast" + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fishing_rod_cast.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fishing_rod_cast.json new file mode 100644 index 00000000..174fbd22 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___fishing_rod_cast.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/fishing_rod", + "textures": { + "layer0": "mmdlib:item/__material___fishing_rod_cast" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___flower_pot.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___flower_pot.json new file mode 100644 index 00000000..af2a7d34 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___flower_pot.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___flower_pot" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___gear.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___gear.json new file mode 100644 index 00000000..118aa702 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___gear.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___gear" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___gem.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___gem.json new file mode 100644 index 00000000..d86fc37a --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___gem.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___gem" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___helmet.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___helmet.json new file mode 100644 index 00000000..5b88d62a --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___helmet.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___helmet" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___hoe.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___hoe.json new file mode 100644 index 00000000..09b07812 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___hoe.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "mmdlib:item/__material___hoe" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___horse_armor.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___horse_armor.json new file mode 100644 index 00000000..75f22fe2 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___horse_armor.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___horse_armor" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ingot.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ingot.json new file mode 100644 index 00000000..ecf6ecc2 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ingot.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___ingot" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ladder.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ladder.json new file mode 100644 index 00000000..f5d16424 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ladder.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:block/__material___ladder" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___leggings.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___leggings.json new file mode 100644 index 00000000..4fdc3374 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___leggings.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___leggings" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___lever.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___lever.json new file mode 100644 index 00000000..179c6770 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___lever.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:block/lever" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___molten_bucket.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___molten_bucket.json new file mode 100644 index 00000000..2ba82f43 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___molten_bucket.json @@ -0,0 +1,5 @@ +{ + "parent": "neoforge:item/bucket", + "loader": "neoforge:fluid_container", + "fluid": "mmdlib:__material___molten" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___netherore.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___netherore.json new file mode 100644 index 00000000..c1b1d763 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___netherore.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___netherore" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___nugget.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___nugget.json new file mode 100644 index 00000000..7b66b119 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___nugget.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___nugget" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ore.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ore.json new file mode 100644 index 00000000..f1da14dc --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___ore.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___ore" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___pickaxe.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___pickaxe.json new file mode 100644 index 00000000..9cf5f7dd --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___pickaxe.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "mmdlib:item/__material___pickaxe" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___plate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___plate.json new file mode 100644 index 00000000..35bb68e1 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___plate.json @@ -0,0 +1,8 @@ +{ + "parent": "mmdlib:block/plate", + "textures": { + "top": "mmdlib:block/__material___plate", + "side": "mmdlib:block/__material___plate", + "bottom": "mmdlib:block/__material___plate" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___powder_dirty.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___powder_dirty.json new file mode 100644 index 00000000..f29cb261 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___powder_dirty.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___powder_dirty" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___pressure_plate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___pressure_plate.json new file mode 100644 index 00000000..d3922b75 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___pressure_plate.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___pressure_plate" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___rod.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___rod.json new file mode 100644 index 00000000..a5daf615 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___rod.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___rod" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___scythe.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___scythe.json new file mode 100644 index 00000000..a0c3c08e --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___scythe.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "mmdlib:item/__material___scythe" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shard.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shard.json new file mode 100644 index 00000000..e9e97cc4 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shard.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___shard" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shears.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shears.json new file mode 100644 index 00000000..a01fbe6d --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shears.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___shears" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shield.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shield.json new file mode 100644 index 00000000..bbb0f165 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shield.json @@ -0,0 +1,14 @@ +{ + "parent": "mmdlib:item/shield", + "textures": { + "shield": "mmdlib:block/__material___block" + }, + "overrides": [ + { + "predicate": { + "blocking": 1 + }, + "model": "mmdlib:item/__material___shield_blocking" + } + ] +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shield_blocking.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shield_blocking.json new file mode 100644 index 00000000..c335b8cd --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shield_blocking.json @@ -0,0 +1,6 @@ +{ + "parent": "mmdlib:item/shield_blocking", + "textures": { + "shield": "mmdlib:block/__material___block" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shovel.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shovel.json new file mode 100644 index 00000000..cfc02dda --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___shovel.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "mmdlib:item/__material___shovel" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___slab.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___slab.json new file mode 100644 index 00000000..aa09d748 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___slab.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___slab" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___smallblend.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___smallblend.json new file mode 100644 index 00000000..0012852e --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___smallblend.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___smallblend" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___smallpowder.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___smallpowder.json new file mode 100644 index 00000000..2c318388 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___smallpowder.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mmdlib:item/__material___smallpowder" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___stairs.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___stairs.json new file mode 100644 index 00000000..fe25c915 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___stairs.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___stairs" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___sword.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___sword.json new file mode 100644 index 00000000..0e0a94f0 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___sword.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "mmdlib:item/__material___sword" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___trapdoor.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___trapdoor.json new file mode 100644 index 00000000..da4ff449 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___trapdoor.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___trapdoor_bottom" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___tripwire_hook.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___tripwire_hook.json new file mode 100644 index 00000000..e00a344f --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___tripwire_hook.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:block/tripwire_hook" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___wall.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___wall.json new file mode 100644 index 00000000..9486b302 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___wall.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___wall_inventory" +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___weighted_pressure_plate.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___weighted_pressure_plate.json new file mode 100644 index 00000000..31711bc7 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___weighted_pressure_plate.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___weighted_pressure_plate" +} diff --git a/src/main/resources/mmdlib_templates/provenance.json b/src/main/resources/mmdlib_templates/provenance.json new file mode 100644 index 00000000..79a0a210 --- /dev/null +++ b/src/main/resources/mmdlib_templates/provenance.json @@ -0,0 +1,270 @@ +{ + "block/__material___anvil_base": { + "sha256": "fe36821a9b622e089847e799004a925bba7bfc937957d80ea44ad6826914f2ed", + "source": "textures/block/adamantine_anvil_base.png", + "untinted": false + }, + "block/__material___anvil_top_damaged_0": { + "sha256": "65c3bad09ff94d8a229b5ca69264b95e89ff649b4a3ac3bd958f4518563c1cd0", + "source": "textures/block/adamantine_anvil_top_damaged_0.png", + "untinted": false + }, + "block/__material___anvil_top_damaged_1": { + "sha256": "dedba5078ad6beab32fe610a7df63831259846a8355eb256eb1c5c8ded6d3222", + "source": "textures/block/adamantine_anvil_top_damaged_1.png", + "untinted": false + }, + "block/__material___anvil_top_damaged_2": { + "sha256": "4da8dcff1e71de22bdbb86bee7d697da4cb5b357a2152a388170f93c42a636b1", + "source": "textures/block/adamantine_anvil_top_damaged_2.png", + "untinted": false + }, + "block/__material___bars": { + "sha256": "55746e0ba402c046109f9f6ae19909f484dc714a8677d480dc09343f288db1b6", + "source": "textures/block/aquarium_bars.png", + "untinted": false + }, + "block/__material___block": { + "sha256": "9a1e68c1a04d92c6bbd69e42770abb30956676a7f9c757977e926ff2aaf973eb", + "source": "textures/block/aquarium_block.png", + "untinted": false + }, + "block/__material___door_lower": { + "sha256": "de96fbf3f1d4491cc62b832537dfa74fa96291f4b218fc85e1bd8e1dd81f2097", + "source": "textures/block/aquarium_door_lower.png", + "untinted": false + }, + "block/__material___door_upper": { + "sha256": "4e012a0eb6cd777bae53827051ea00f2d9a3d9e24f7150922a6176094e43637d", + "source": "textures/block/aquarium_door_upper.png", + "untinted": false + }, + "block/__material___ladder": { + "derived": "rails and rungs cut from block", + "untinted": false + }, + "block/__material___ore_overlay": { + "sha256": "76d1bb9c9eacb71919c5f01ab8c8e79080338b1421cf0c731206a5908a02f034", + "source": "textures/block/ore_overlays/antimony_ore.png", + "untinted": false + }, + "block/__material___plate": { + "sha256": "9a1e68c1a04d92c6bbd69e42770abb30956676a7f9c757977e926ff2aaf973eb", + "source": "textures/block/aquarium_plate.png", + "untinted": false + }, + "block/__material___trapdoor": { + "sha256": "35f7c3343a428640bc86e6a332627e46a9f397eca6c5475a30e308b0af36356b", + "source": "textures/block/aquarium_trapdoor.png", + "untinted": false + }, + "entity/horse/armor/horse_armor___material__": { + "sha256": "d105a1aa619dfed486dea582d5069489d13adbbedf7be0a4a7cafabc6150ea88", + "source": "textures/entity/horse/armor/horse_armor_aquarium.png", + "untinted": false + }, + "item/__material___arrow": { + "sha256": "7e1eb191e357a119f759d8b74fc2900a17fdf9165ca34193dbddb3ee7e84a681", + "source": "textures/item/aquarium_arrow.png", + "untinted": true + }, + "item/__material___axe": { + "sha256": "15961910441163b00a3c74e3b1fd641624f33adbddd0b80e4c7f18be486bfcb5", + "source": "textures/item/aquarium_axe.png", + "untinted": true + }, + "item/__material___blend": { + "sha256": "d3c5a6dd548f4aadb9039e9d7e75adb2a677387957bc8ee3dac7e53978f3f1f8", + "source": "textures/item/aquarium_blend.png", + "untinted": false + }, + "item/__material___bolt": { + "sha256": "a4a827bcbf20567560453571cbc62abed219d3101ed95e54ab30b6d71ec99b5a", + "source": "textures/item/aquarium_bolt.png", + "untinted": false + }, + "item/__material___boots": { + "sha256": "96cfac1c139950eaedf91ed47cb7b42c9acc2901e517df65e66db9f10fa4544f", + "source": "textures/item/aquarium_boots.png", + "untinted": false + }, + "item/__material___bow_pulling_0": { + "sha256": "6e9266f5199e1080b8de6d8432a8ab45ce12f255d856b59af903b1e510112c94", + "source": "textures/item/aquarium_bow_pulling_0.png", + "untinted": true + }, + "item/__material___bow_pulling_1": { + "sha256": "2de4e743d42000bf48a1df98a4faf799392b7b9c2962a898636b4d0b5e0cf938", + "source": "textures/item/aquarium_bow_pulling_1.png", + "untinted": true + }, + "item/__material___bow_pulling_2": { + "sha256": "faadf2ed5398580483a14f6fb3ce98945e52ebc6fc10fd4cf94c3023f67b5886", + "source": "textures/item/aquarium_bow_pulling_2.png", + "untinted": true + }, + "item/__material___bow_standby": { + "sha256": "9f1a0a3ebc96316c8777148a69051c0b331dcfbc2968b9e2b824093edbfbbc20", + "source": "textures/item/aquarium_bow_standby.png", + "untinted": true + }, + "item/__material___chestplate": { + "sha256": "9c1437952f31939e7f3f84c569f1987cfa25b1d505a147556c313aff4dfd0c16", + "source": "textures/item/aquarium_chestplate.png", + "untinted": false + }, + "item/__material___clump": { + "sha256": "848ef33d934f477f84f00b9253b021d6b8172dd6716386e955884502fc302dab", + "source": "textures/item/starsteel_clump.png", + "untinted": false + }, + "item/__material___crackhammer": { + "sha256": "0eb9af2fef27c07d55a7453bac27e80ad28db3ac93c0570bdd9601d5e5a92ec3", + "source": "textures/item/aquarium_crackhammer.png", + "untinted": true + }, + "item/__material___crossbow_pulling_0": { + "sha256": "5fd9cbae7f6cf70a57fcae39dee2705b8ae2076d93c4c4385452103b81ef4514", + "source": "textures/item/aquarium_crossbow_pulling_0.png", + "untinted": true + }, + "item/__material___crossbow_pulling_1": { + "sha256": "cbfe8ceaa8d915a79d0c177c1b7ef74e5d5242eea9e5768e54c6558d9cccc90d", + "source": "textures/item/aquarium_crossbow_pulling_1.png", + "untinted": true + }, + "item/__material___crossbow_pulling_2": { + "sha256": "3e7b70d705ab5b3d51d640b26f53af664f5bacb45e2f8aaabaa57cbd180291a0", + "source": "textures/item/aquarium_crossbow_pulling_2.png", + "untinted": true + }, + "item/__material___crossbow_standby": { + "sha256": "0ef60e11f856dec83e3e125eb92c4c449bb621cb5b61ace1db2d634fa02b1508", + "source": "textures/item/aquarium_crossbow_standby.png", + "untinted": true + }, + "item/__material___crystal": { + "sha256": "83f6c79204a97b956d5ca67e2459c44a3e9d1810e94ac33225572d256f9bddac", + "source": "textures/item/starsteel_crystal.png", + "untinted": false + }, + "item/__material___door": { + "derived": "door halves stacked and halved", + "untinted": false + }, + "item/__material___dust": { + "sha256": "d184a8105999118f13bc22a995660085dfb8ba346370b33de1db3b47d10e1ee4", + "source": "textures/item/aquarium_powder.png", + "untinted": false + }, + "item/__material___fishing_rod_cast": { + "sha256": "27c4b78d134165952b331937c865ef0ddc641bc4d6d7330bc01d0319bb9fc1df", + "source": "textures/item/aquarium_fishing_rod_cast.png", + "untinted": true + }, + "item/__material___fishing_rod_uncast": { + "sha256": "605c65b849050b44c1c89b1eb6478a1da52a51344ab0fb726b1ab62f02c986e8", + "source": "textures/item/aquarium_fishing_rod_uncast.png", + "untinted": true + }, + "item/__material___gear": { + "sha256": "7c216b3086fe72842da2f1eeb0bfc0a1bafeea9fd55f715387e332c2bf45ac59", + "source": "textures/item/aquarium_gear.png", + "untinted": false + }, + "item/__material___gem": { + "sha256": "83f6c79204a97b956d5ca67e2459c44a3e9d1810e94ac33225572d256f9bddac", + "source": "textures/item/starsteel_crystal.png", + "untinted": false + }, + "item/__material___helmet": { + "sha256": "b3a50cc52af22945f555ef2e4bdd31adb40d16e9f738775f45603a3825dd0c57", + "source": "textures/item/aquarium_helmet.png", + "untinted": false + }, + "item/__material___hoe": { + "sha256": "8cf36c24baaf8eee1db7c2a0fdcb108e39258be55048dd06d9b0e6d615249141", + "source": "textures/item/aquarium_hoe.png", + "untinted": true + }, + "item/__material___horse_armor": { + "sha256": "f0d1ef1f2c639594b8a1eee1318f8ea69144840a814ba5994529c361896824df", + "source": "textures/item/aquarium_horse_armor.png", + "untinted": false + }, + "item/__material___ingot": { + "sha256": "71ab16d7d0be039f3e64c183e4e260e101a245f4b87307edc61338adbc5c8391", + "source": "textures/item/aquarium_ingot.png", + "untinted": false + }, + "item/__material___leggings": { + "sha256": "8c9d5639bd5073df28d5d996a4a859019fd0ed9bb8084f3fa1c5821cd5080ef2", + "source": "textures/item/aquarium_leggings.png", + "untinted": false + }, + "item/__material___nugget": { + "sha256": "9c117529b4e8aae3bc29d0acfc8a7f5faa13b8e442695d1f22c67a3e3c033b9c", + "source": "textures/item/aquarium_nugget.png", + "untinted": false + }, + "item/__material___pickaxe": { + "sha256": "71ccc5796b930d48601047fbb515d456a4ac2891572d549a58187843bc2e8ee0", + "source": "textures/item/aquarium_pickaxe.png", + "untinted": true + }, + "item/__material___powder_dirty": { + "sha256": "7c9678d9bb353094dee03f155e0c63571fb229752256cf7ee8f7373f1884f6e4", + "source": "textures/item/starsteel_powder_dirty.png", + "untinted": false + }, + "item/__material___rod": { + "sha256": "a4e451a068a6b8a6849d5d7cd928b9f75f2a67e81bd4acf90c2a4deefdf67681", + "source": "textures/item/aquarium_rod.png", + "untinted": false + }, + "item/__material___scythe": { + "sha256": "70c932a111557cc4a77420c60b72808a191e781cd156fc0a8697b9c0f7aff255", + "source": "textures/item/aquarium_scythe.png", + "untinted": true + }, + "item/__material___shard": { + "sha256": "f56231997cd4c28665865d2966ed92635a760806fb17404ee5fcedfe123569a5", + "source": "textures/item/starsteel_shard.png", + "untinted": false + }, + "item/__material___shears": { + "sha256": "1ab29d5ed2c4ba81fb2b8ce74bd910451a5f7208e4d9e631d42767fc9e239c48", + "source": "textures/item/aquarium_shears.png", + "untinted": true + }, + "item/__material___shovel": { + "sha256": "20723037c47d7e44bd74110dd183b9fb705a29ca14ab94bddbb1237264c3555a", + "source": "textures/item/aquarium_shovel.png", + "untinted": true + }, + "item/__material___smallblend": { + "sha256": "2e6aa2e88515ffb2faa8451c3cb8957b2ecf8a49abe226fc9d29741cc0806775", + "source": "textures/item/aquarium_smallblend.png", + "untinted": false + }, + "item/__material___smallpowder": { + "sha256": "afacab5646f0601d7957c0dc3a57f0e03ea33cc5e0299f6181f67b8e2346c42f", + "source": "textures/item/aquarium_smallpowder.png", + "untinted": false + }, + "item/__material___sword": { + "sha256": "ec06fecc7cec7d97cf096cbe4fb20652f72c450b8a76fccb3d0dd50bc15888cb", + "source": "textures/item/aquarium_sword.png", + "untinted": true + }, + "models/armor/__material___layer_1": { + "sha256": "3287829ced564d4ed599d37b26eaf8bff76ade035189e2accabdd3a95405b5a2", + "source": "textures/models/armor/aquarium_layer_1.png", + "untinted": false + }, + "models/armor/__material___layer_2": { + "sha256": "c609b895a07692faca7e3596f735904f5364ce6bc52f43eb0cd7d9fb924c8efd", + "source": "textures/models/armor/aquarium_layer_2.png", + "untinted": false + } +} diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___anvil_base.png b/src/main/resources/mmdlib_templates/textures/block/__material___anvil_base.png new file mode 100644 index 0000000000000000000000000000000000000000..88aece00a7b697d45f3b56c9f9368a1e4a8ff6bd GIT binary patch literal 390 zcmV;10eSw3P)`o?=tYI8S1Jg7) zmSwSh-%TGe-?nYOpXb^6ecxNxbr#r=cpQhF=V|wSKM4r2@^Q}nFbt1KUDscMrfFP6 z7wfuuqLPUvh6`5Z1#HMZL`?DWm$e5Xo!&X8tkzS)E2AUXjY%k=XIcGVFeH*O3WU>fe*+HSdj}3 zs1y%Y0|oJr5kn*tMPYz9rKh+8)wOC5;8K)47oakV2tc4mst~J1JtOC`?*d7Zc%bUB z3-)pEg5oqy-xd&28|;~l=0gH^|J}O))&4%Chyp(i0&*v}&kP9S3PL3y#;{X5v literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_0.png b/src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_0.png new file mode 100644 index 0000000000000000000000000000000000000000..a2dbabc83737461ecfc19fca504ddf972fd6e417 GIT binary patch literal 290 zcmV+-0p0$IP)lzj)j0WQE!DThWHMerpecJS+f)YPRN7M z0SZ7)7>0CRSA%Mw1X80J0-pm#0;%{{s3D*GbO3lv;5<)*uN4VU(f~>#0d-y5`L_eT o%9PSe91?J8a-m`EE>b`91LO2Q@TRE^`~Uy|07*qoM6N<$f|L+>(f|Me literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_1.png b/src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_1.png new file mode 100644 index 0000000000000000000000000000000000000000..001883b1c3d4929e68e2058c36c49a47e50a8542 GIT binary patch literal 336 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU=;UsaSW-r)jN6Psx||j*4ldW z@Ph|E3j}0bfS9EairEVbrp4@YTGIVD|G|Y>OVrgfzuKJMwQlmZ+|*fWmVM6WK2O=C zbGA;#T{wG|+aCSf-|wn0pAdPaJn#8Wb4JmwMFvwvN@Mr03NYk3Y|hB3qBOfUG_>jF zr@6W}rY_c)^n2ggMvK1tV%u_ywJ&4|Z1^P0(wL#c<81CYN#&WrrGK z+Q`AqWlICoIuVQX4eM4jsGZ+3|9R!=tTUF(Da;&Gj(o3u`TL$jvOQ;xs=y7q51tD` zcbK)F+8+PRpOwdZzP^8Xq}WA?0=|hU4yu1AMqD;AeZ60VVN&D1GnMn=lhhrjrLO0) gU9(qvW$iP5w%JncE{pW`0z-nh_g4ku9GB56p$G}0L=GY0u&yPhrC`d z3BynoNnnjDTwE@feS#>8^dPp^b(Qn^tTQn7ttHI!{8bKuK-iN&3^Gb++jgG-i?}cZ z%)~s;Qx~jHr_+A|l8quUl8FPxE&=S%=Tm`wh$sOE(=^HPc(nI`d%xfBYLjj+%kotv z9>=l02U(V>3J&r-R}$BCrLJoQF04I(fI}#u2nhs`vIo{4paKckW?+Wd^D_UIfQxY) z6}Y+GZW;re*(Koc`~B7glz5Sl_`WZP!$D`O19MfVkPNWRzp-tblC!s#z*XV^@DfQj ef2DEsoAUuSQuf9J8}TCm0000Bt4bF-_6?Zbs02?lxV=qI>vMkOp zT$LrjOG%Q%6QKUtZ1!goITQQ$9U@56l>8sEEF%GXzu%nH4*{F;uIpgAT#|(1?OoRe z#5SAFumn~wzRtderyJZLj$@7hQ^%$gJrPCmgZ%)Sf=|x&6q^+Q0000 zzh2dsA9$QSOQ~17*!C)Zh_xl#T zUQf{NcG2l{gh&Y3#PxQ&VYONb(Qr8YNJIkGl0*n7WxL&Cv)O8 zJYv7!V>X-Ndc9gZqtVC`D2Z!@If;eIWFjY%fNFvuz+$lwg0KpgKol@l7P>*|>HiT# zqfwZAJ}*V{`CKw^{qJ@=QS?^^wOS44e!t^p zOG~n05Qd*$=HJN@7w$ybxX>^a@sP(5iv$ZL#jGer1yNB%NsBDVT?FDz7p~^x8hGV< zVVLth_ssK}i;D{x3{L}yr zdbwQ6$H#}v=W`XB(X-i1rqihc0rK+lBE@1+%-7e~0bsk`Y7tHN2zIa6lTN22tyW9w z^}04cJw3_w^|hGAVxj0z;-je9Y-$lrG*l`Txx2elDVNJqtJPHU`Mj9*dM&HfO5We! zb=qdLIqZynetuRk#F`Ex;+90jsbpP0)ROe3V9`p*}3}bp`120uK)l zib{fp*Vk8F;r{+!W0nShdhDyKD>2Z73j6VK*_)dim2S7IVC1cMsZ>%a6be$UR>jZ+ zZ9WnJrBW%$WHOS?W;LEpr&Wlve|oQxir?Sg`hUQH z{hN5p{L7fWS{eCY$KR^293{VE3ghCKR*|qoqJGodU`4bS~L=X1riR2Rl^J*Y-%`WE|=5kR^C93BzJ@o23#4c)^xxu{l5(>i>gZ literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___door_upper.png b/src/main/resources/mmdlib_templates/textures/block/__material___door_upper.png new file mode 100644 index 0000000000000000000000000000000000000000..3c43ee865f199b828bd789858818418b893c76e3 GIT binary patch literal 469 zcmV;`0V@89P)1a3{YCfOqcDre}+u43Pof@}Vt+ZGywBPS_xm*-D9FNBujYjHryIL-nX2=D= z8Tj7&I+MvLxN@)8)8p|l1J?b1-}u2`@Bv(-(fBwt0U{tQi172lw_2?~JWdfyBoYd- zSWM^h*>d_d^!%fEL`tQSLOdR~P@_C;w_Ee{%WpOtO(qlNaybPmiok3(vpx$u&xtdBHxJOe!!mY>0$<5Z*U9^EFfYe!sO|uT`y9l}@J> zD3lwVPA3DN{ZD$-0`cqhY9bVnw)2hx4+bNzs?Yv+ZHThjY+7e3m5N;kg+k$f7pMV7 z{!q9G@Cpxy19!v#fnu?!aKGQJuc&kAUI>-p^8M@evgvah-U0XrWqq98>?l__00000 LNkvXXu0mjf*_hp4 literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___ladder.png b/src/main/resources/mmdlib_templates/textures/block/__material___ladder.png new file mode 100644 index 0000000000000000000000000000000000000000..e85508addfb340800574f1a22b8517bc77cc1ee2 GIT binary patch literal 294 zcmV+>0oneEP)DmT-U`~aophV)umVrp?|^^W+}k6{(w@1e|6FPYJR+-F2yl!Ck9?`Il~z8=E218^u7!yZ(=Fq^oXd&}7u# z^Zn49^>dC&e4l=D)8md*Mjz&lPx-epC_TvHoN@E=*QwFJ8D?3(+4gj^SvJs(44$rj JF6*2UngFZQSGfQH literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___plate.png b/src/main/resources/mmdlib_templates/textures/block/__material___plate.png new file mode 100644 index 0000000000000000000000000000000000000000..06efb3fa040da96ab553f8311acbf77b30fef072 GIT binary patch literal 455 zcmV;&0XY7NP) zzh2dsA9$QSOQ~17*!C)Zh_xl#T zUQf{NcG2l{gh&Y3#PxQ&VYONb(Qr8YNJIkGl0*n7WxL&Cv)O8 zJYv7!V>X-Ndc9gZqtVC`D2Z!@If;eIWFjY%fNFvuz+$lwg0KpgKol@l7P>*|>HiT# zqfwZAJ}*V{`CKw^{qJ@=QS?^^wOS44e!t^F2#-Bc;l6%d>KwA+By?BBsu@t7pJN!<@>%0f`I-!|&S6$?6vravkuu2A-b) zfN#Xo#0V4dW3j;erid-<&NhgBXSp*&#BYx~JP8vlBW8Nq{+E9Y;H`0FG$;S&m$TfN zwqLdZjL__$mg)1(8`u^t|UC6Q~hhqJ&H7hes08yYIJ5dH)bQ}+n%Ekdz4J0==~CYBll z$LH+|&doI9N2aK%u8x>2_V0fApsZ4x+$qa}9#H-K$4rHmL=lBO8lW4t5L|Y?C@c+u z3i~@$9j@iuqnKpn(Qq}Zg}|yJ?5sbP)bhQjOW3S>H@{x=)_+k6dONJ+rMD|PU)qkx~+acVR zKg4}O(AGLddOywljrQBy!>>{#b&ev`U;i_x?^&4K4`#CaS!vf*s>{oR<;_#s zW#Apw^778c-#NIN+po0ff19_Wch+yz=lpJ9R=YQSvCTXE(YD~+H$lZu-Q8nbE7Qr( zyac6?ldZ(v`K+NYp5yks4^UKI`eb6iz8VOGh8FbiAB=81SxlXc`@6UMkEnci`{L_} z+K0N~Rj5PK=f1A>7V3{WU$}7FYscT|gBRg);XZqN&9)P$Z3LjaM)JWlDpUGw$h(5G zH#SDad|EM=12eL5QRQ*^b>4F_LY+__>##xVv-w_=m>;v?v$A%rDoG z_KDmhtb<9NaTSBf8E%=ige@vZ^3gHc3t(gyo!OO!aEuAX8Kg0D*Tf#`Xo&Tt)HZrf zTy7(dU%6y>7bqS)zck*`^lKXAzlcni(M{0QBW$X+){$rFnACo~cgmhm+XO51@hhR+ zp4l=cIHzWG7e|c*Ypxezt_NNk+lql}hW>@MLCe98??KlyN1|aE;@sO6#KlfWgW~}U3 zaz}x^po!L*-HPusrZ)vcl(_fGCrWI|VimBM68@tSa_OR11$9(<(}3cOTRhREjd|xU zY}tj~wZr)<#_v2D10ce3@sJOG?@}N**X%$twHFPeMU+NguLKY%;(CX-fE*U`Apyn7 zffXgM=D_FNQdh88tZuXahLbB@baQiYv@m25yvh)y_=zAsyt+BJ8WJpyO0m2qE8Zi| zBQ19FqQxy882VSft-N@;ThW*>u}_M028M;L&h4c3|I4nYTc{~aQ6r+*EAM`ayXqNU z18Y_ky|)n_M2t_6|0vGXwi@ZYlhMha@|_~>??oqX8JnUloSmJQCfj2R0dX@$5eKu1 zXq*t%^I3LG86Ycx$!9PhzG7(1mcrte@@+7=EZ(8s$p!Io8K57X82IUDGnqcaB zq*Iv;1Ty#$iFCo*0pNPVwA4^InIPKk*7USaG4 z$Et`S1-X%=!y|JaVz{#?zzKkWJ8VImf`&TJUjAJquKDq zSU~6)S|Nj~$t(_5W9blyr0vxwP5Fw4csUWQ#!ABEXLxu&WAc6E*0<+aLYdPrL}O#B z34ep*J#YTydwpt!AWfe;3;9AF=f%P)|MKBuQYcn2$lB>QI8y{?ZBdg1uPWSjGL70d8BHn-~hzo(Q|c5#t|Oc-z+`V^ze&N{oDWPLV0_k z-E=<-?ZdQ_GUI;VF!#79e=5+(K5y-m5#UpW1ZO8+0$NM&$e5Y>jj`8u^YULrr`tk! zzRF}9!evgt4G)4C*_*!5RtSzbW_uevVz=s*T8DRdtz14R?aiWsjUL4-m64h=dx(}?sg)OGKe(GvB@To_W>w__^X;-lBc99ZB@eJ{{pXG?;gMXIPg~*&K ztKKWxxWx3BMIX{nVQ8%#dJw=%X8xJF+n~@5 z=WF+q3wn#(bqTbL<#}8p^Sr*MBc()5{k8z3M|IOa_hW#^ai1F|JTGRdJc+M5BVYz- znre6b>Y_*3zWv#fp}6H3A0v=V$R5(q6^W#gT@ziF{5yzmFiE#S#bCqP%H-P%GOuq- zso5qanFdoQ>~IQm!z7>vrLoP1=_Y9a0+}YNZAU$iOesJn$hX#{m%{J;oX`P*Bq{@|CVR2MU!td<5 zaozE{feH6yUp+%{#%}Go$K@j!jj_vyd>ka9%CECTT9IG4#z2m0+t1bMT(4rzD?bNJ zkI3S7yhPyai)m<=t6^2W^7VD#Blx%6rLM4Km2_7z>qGJWx2Q#FBTekRP)Rcj5T_CB e|AUGUwqR96youS$Db~TS4_KPonN*>1G5-Uw5wYw5 literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___arrow.png b/src/main/resources/mmdlib_templates/textures/item/__material___arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..ce689ceb88957b7dcae801a14dd85fed40ff166a GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`J)SO(Ar-fh6Bh7z7+<*CFkL_X zANxg1OG|~QIT^k3b~Qix&**7rFnny?url?KnuN6U@BNDwG39s!1`3`ruv-)x5Eu6^ zJ~HyBJ;#)3(+=@GQ1CSF=~X%E7VvpX=H&;oI=|}4H2ld)P5mn0t)|vAYvNpABZkbd vprBv=WuF##7~fp5WXThIBj1UNC9Dh;%=Qmlm&-8&-NoSP>gTe~DWM4fFpf#u literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___arrow.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___arrow.untinted.png new file mode 100644 index 0000000000000000000000000000000000000000..2c753c01da156215f522121e138fdd6ccc78b1ee GIT binary patch literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`=AJH&Ar-fh6C@@*xVAR>|H(@` z($CNPui!KBZ9AXruk!|6Y{nHI9vu9ie7x`Pau4HN@i4y!&*NW)$BOwRt-E!Y=*Y zG1;!~WgmV%CXun$F*ML?r~Q&sXFXP+qiuYUVk<3*c-b85HK1Qc$kc`-7?3Tr++SNZcK P&>RL&S3j3^P6PR<6Pt6(ezBTsKDh**&XXM+x}Yj81~lap0hO1erv_zxb`cVgjS-PvGn)e(HoDwM@4aDJzGGKw&$iUL`qd2$8v2S-vs#k9I=CN+2_(PSzDktw z%B9CT77I>P-)BF$w&;UJ^XJEs)oRo4H#-!4z573vorV2N$cL4cpMef!@O1TaS?83{ F1OV>)QA7X$ literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___bolt.png b/src/main/resources/mmdlib_templates/textures/item/__material___bolt.png new file mode 100644 index 0000000000000000000000000000000000000000..f3bcb4ee7fa7e02a30372bc5bc3e04cdc2c33b83 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`DV{ElAr-gYUNYo5pup3b_}<=C zae0BLdtpiOb=w)pOz-JBb?&=wl9Ue9xH!-Z@()xFf?Y`)TxxtLj zze(S0C|5QM#)f<>_NJpQ3K5)!br@fPL+LB&Fgc}zeHd7^KNH!$UVyKE*w@K04$g}ScM zG>!MGP+69s^E}5iP3XD~ecxl>cf@i09g<}kilXpdmIcNbtm}#>ica5#hhczo4q9u> z^NcVIPd~29$yA!ANRs4T=n81t_V?f+Ypp+&2r!PLUtcLDo6e`=wZ-cba9mv!<~xh0 uv!ZMm3nB)vo?-w$$hK{A0Cn5#2Ymo}5vLui{_G+E0000V$5 pWzPOBP-^9&>^iG`!rCWH3|5QM#)f<>_NJpQ3K5)!br@fPL+LB&Fgc}zeHd7^KNH!$UVyKE*w@K04$g}ScM zG>!MGP+69s^E}5iP3XD~ecxl>cf@i09g<}kilXpdmIcNbtm}#>ica5#hhczo4q9u> z^NcVIPd~29$yA!ANRs4T=n81t_V?f+Ypp+&2r!PLUtcLDo6e`=wZ-cba9mv!<~xh0 uv!ZMm3nB)vo?-w$$hK{A0Cn5#2Ymo}5vLui{_G+E0000WP} oyX7w5o;Ja4lgZ_H<(&)+C%+hNwmj-r1ys%8>FVdQ&MBb@0Imud00000 literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_2.png b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_2.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd644aedc32fc9bef26f3b73075732ba2a5da51 GIT binary patch literal 244 zcmV|5QM#)f<>_NJpQ3K5)!br@fPL+LB&Fgc}zeHd7^KNH!$UVyKE*w@K04$g}ScM zG>!MGP+69s^E}5iP3XD~ecxl>cf@i09g<}kilXpdmIcNbtm}#>ica5#hhczo4q9u> z^NcVIPd~29$yA!ANRs4T=n81t_V?f+Ypp+&2r!PLUtcLDo6e`=wZ-cba9mv!<~xh0 uv!ZMm3nB)vo?-w$$hK{A0Cn5#2Ymo}5vLui{_G+E0000UF< s<0*X3zvjQ^HJ(ciJ)YNGrmOHWoNu%IcxBG=QlJh7Pgg&ebxsLQ07&T^MF0Q* literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___bow_standby.png b/src/main/resources/mmdlib_templates/textures/item/__material___bow_standby.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd644aedc32fc9bef26f3b73075732ba2a5da51 GIT binary patch literal 244 zcmV|5QM#)f<>_NJpQ3K5)!br@fPL+LB&Fgc}zeHd7^KNH!$UVyKE*w@K04$g}ScM zG>!MGP+69s^E}5iP3XD~ecxl>cf@i09g<}kilXpdmIcNbtm}#>ica5#hhczo4q9u> z^NcVIPd~29$yA!ANRs4T=n81t_V?f+Ypp+&2r!PLUtcLDo6e`=wZ-cba9mv!<~xh0 uv!ZMm3nB)vo?-w$$hK{A0Cn5#2Ymo}5vLui{_G+E0000Wdo hSyee9OO2g@;qqfF5FqdOzVCw=g-nKFh;?026ve5S=eBJnJlnR7iJyccg zhFmTe>~=deO@rg{h{0fh_xp{yuCZJ$8LSf!$1xi$iUJ(RVX;`ivMh{7BODF~D2mb! z2!a4qx7!UAgTxet?3ZQ9AUw~v1EMHm*0SV7829@f zo6QER)e2!4g51fn{QDo!*+@kB>3Il9wyFQo6G@W91SF(0b}sEt)EB~D!?oJo6n_8! N002ovPDHLkV1fZlrd9v| literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___crackhammer.png b/src/main/resources/mmdlib_templates/textures/item/__material___crackhammer.png new file mode 100644 index 0000000000000000000000000000000000000000..24c9d6bb6636bd2bc49db0176b73fa2d12fb5772 GIT binary patch literal 214 zcmV;{04e{8P)~w#u$dD zNGVYfLiC%|$tfk2QgEIp#&K*nsm=2|!+U?%x~?$B^rfgK(=^?}@9IwS7FuiJoWpS( zpp^Ed2_YcojO)4(W5l*??MVnB&{|{P_m9D&(<-H~EDJd2&;O!pfq#C)3)YK@j{83Y QF8}}l07*qoM6N<$g0X8{z5oCK literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___crackhammer.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___crackhammer.untinted.png new file mode 100644 index 0000000000000000000000000000000000000000..e11e102908f771d199d5e55b40f838c4288f5b69 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`7M?DSAr-fh6C{cbT=w}=-!}hF z{U+WQKH>k@D<1z_-|=&&Vf=Udzsx*KX1-x)zUi~=*~GNZ4eI_3KWFNNb}VOl2sDGi M)78&qol`;+0QDv+7XSbN literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_0.png b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_0.png new file mode 100644 index 0000000000000000000000000000000000000000..e4edc552de98b10f8929df530dd46bc8c437b123 GIT binary patch literal 313 zcmV-90mlA`P)GZhg74`3={BzOQd6cq#oLl0nP>b+{IVRaeT9ea{}@M!3MRj=Ns zACLbu*LC4}-r>R^y5l%#ng(f_BFi%LeGgStuSsm%#tv-T7DpIA?e7K@0*Wf(Q zQP=eo#Eg<89eyj9nU`h3x~>SrPz-b?h~pSMnVltkL{L@Li|lRN5JeG$!NV{>)3gJ? zX3>O{Fo=yi*KJwWlcXpL+!E~a&u^w_VpsVmm~(#DbV$5 pWzPOBP-^9&>^iG`!rCWH3GZhg74`3={BzOQd6cq#oLl0nP>b+_ydFx|+vd$*2p&8Jw>gt}F z$KyZ4bzOL#ccd_g={OFWra_vf$g&K5-$PZ^YZBYG@dCDOizG?Vbsc0`K2lMXYjB?D zsOx$Mv7;nON8ZY1=Ve*2t}DVY6a$?J;y4CRcIOCR5mZ(6CVSg9L{S7`@GuO}H0=Oz zSqvd1EaKwMb6b|RlN3dPTY{JT^FPxx_i;XgdCzxSr+Lb<#3cb8#+g467>0p)p5gmG z{t(cfcLWSNa2!VjLGWA4he3HF1Ps1`X_}zxI^I5hWP} oyX7w5o;Ja4lgZ_H<(&)+C%+hNwmj-r1ys%8>FVdQ&MBb@0Imud00000 literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_2.png b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_2.png new file mode 100644 index 0000000000000000000000000000000000000000..b512b14dad050bfd8ec14dde593e562f0f873320 GIT binary patch literal 305 zcmV-10nYx3P)8 z)dpfmS(f)amCMfaJY!iFgkdNKIugWj41U?27A_H#W%(j|T~|a=1YvOB_t15H2XI&n zAtfy0;Euj6%i2haqQEJ^L;mrfX_}iIs{9J(J^z|I@14u@m4F*Y&u3PrX@c+jcq8C? ziV#rAFboKS;91LuL3tts48DPJ9AOv+UJkG9%)K5rwVAvN#&}F@00000NkvXXu0mjf D=X!#^ literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_2.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_2.untinted.png new file mode 100644 index 0000000000000000000000000000000000000000..88e7cc7c17ba508a576c32a0cbe486d1ec57397d GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`YMw5RAr-fh6C_wun%Vi~{>UF< s<0*X3zvjQ^HJ(ciJ)YNGrmOHWoNu%IcxBG=QlJh7Pgg&ebxsLQ07&T^MF0Q* literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_standby.png b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_standby.png new file mode 100644 index 0000000000000000000000000000000000000000..5629130cf54d79e669a0891c6c95e0e6bcf13be0 GIT binary patch literal 317 zcmV-D0mA-?P)+qNMLLov{vAdX|uPSxoOw+O1LVvXXGX+=>aMpBj~ z#&LwAC~p8K5mG`))VTe<4Z~pHMN#0AfOma=rt3OWLMl(ee9mVDXzpa5LlUwmIqYg| znx literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_standby.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_standby.untinted.png new file mode 100644 index 0000000000000000000000000000000000000000..5b3818fa7d4e25c893494e542e58a723652f51c9 GIT binary patch literal 84 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`(w;7kAr-fh6C_wun%Vi~{>Wdo hSyee9OO2g@;qq#{bti}&8a@xa1$6QLJyj2Zux0_VFC<*~P zk>@$mG{t(oZfAjAU?vPEXG;mkanu5n$wY!#G-mL8K5;&urI5@-DNvRrXybmr%a=mz z3I~kYCquvTtR92N^_=4~IkH dgTdgRz!x?zZ)Tkg%Vhun002ovPDHLkV1luxtq=eJ literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___dust.png b/src/main/resources/mmdlib_templates/textures/item/__material___dust.png new file mode 100644 index 0000000000000000000000000000000000000000..0f459dbe1d6a3c979f104056b8c376a5c49d6459 GIT binary patch literal 279 zcmV+y0qFjTP)m%U$g&JYQJ`rWj<{1!waesrp3$}~k|cqy>zsR@_g#c8bl0dt_-Sw z6d{|-vY@Ie_`VO@w$b-J%Cbb7rVv6fPDN%L4Ys}v27csY2tGfMQ|L4acPmNsxpaSn&z|X<)?fI dz2b+z{SMqk*lLVea4Vy9zSF6&(t8Vf^&|C4Bggj-NJliZ+S%a`9coHtzokL zh9)sy7iaIBIk7OC_g*#cdCRY9d3E8Q?GC*Ed920#ZOf%+w{tAj1Foz)DzFS_ OFN3G6pUXO@geCyAyFY0F literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_cast.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_cast.untinted.png new file mode 100644 index 0000000000000000000000000000000000000000..278d096feebe08ce9ab5d753519675b4e5c70b9b GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`DxNNmAr-fh6C^l3{6D_>zk<)U qw5}Yvt2Qbo{C!v4M48+87#R3}m^sFMu1N)|XYh3Ob6Mw<&;$T29~xr- literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_uncast.png b/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_uncast.png new file mode 100644 index 0000000000000000000000000000000000000000..ed1e7b2c601299f49b884944c157ec3fc7c67062 GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ii4<#Ar-f-UfRgXpeVu|@Lcfu zhL84Vy9zSF6&(t8Vf^&|C4Bggj-NJliZ+S%a`9coHtzokL zh9)sy7iaIBIk7OC_g*#cdCRY9d3E8Q?GC*Ed920#ZOf%+w{tAj1Foz)DzFS_ OFN3G6pUXO@geCyAyFY0F literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_uncast.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_uncast.untinted.png new file mode 100644 index 0000000000000000000000000000000000000000..0d3dda9722755784fa53a1c221d1573cbe9739b1 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`9-c0aAr-fh6C^l3{6D_>zk<)U zw62_Gv(hH<99}jfgSkLc)~f$kdjTyk=}NW6KE!br>mdKI;Vst0Ms`t6#xJL literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___gear.png b/src/main/resources/mmdlib_templates/textures/item/__material___gear.png new file mode 100644 index 0000000000000000000000000000000000000000..9beb845953484896ddedbcc99844c77632de2b1e GIT binary patch literal 305 zcmV-10nYx3P)eGE<0a?n!e)JyiT8vn-=D zO<&)vK>WC_i^g#zPR#RMLHHf8IF8K_e8g&+hN`Nfq9{}V!XQMFB<@|D=cxdQvMe8f z+>N#Owry#iXIhqp`o4d)!T|Q<0pS6axGIONB>i9?qA2?2;DTPeu2UZ9VhLdnAbtR{ zg}Sb37zQf<;(g!AI8NRPJFmBJ-#1OuMBBER#nNqk{>O1BF&04Uy1pUYpK0c77$OWq zyH8wAnx%_nj`KXQn3Ew6g5bA}Pop6woMUlsKx%fs_ z!TzRl?+t>|AJfd0I@t5W62v2gP8>L}Nx68=-w%o{e~zmk|LCW=)FGSqn1s=}9k=?<`#i-r*5?MF4T4CnW$y` z+`+5T(7BFj&-aj1~WF?5=mI72y`EVr>mdKI;Vst E0FZi5hyVZp literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___hoe.png b/src/main/resources/mmdlib_templates/textures/item/__material___hoe.png new file mode 100644 index 0000000000000000000000000000000000000000..e50e8d78d0b4a5eec0cbba0a1c86927ece045d85 GIT binary patch literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Hl8kyAr-fh6Bej``1b7||KXHO zw#Mk~d5wlQ+ZsMRJluZLoFz@*gn+;@4IhVf43bXUyc5j QKyw&8UHx3vIVCg!0R8tS-v9sr literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___hoe.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___hoe.untinted.png new file mode 100644 index 0000000000000000000000000000000000000000..8e89c5ad6c439483adb93339653bba022476e2a8 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`CY~;iAr-fh6C^4RT=w}=-!}hF z{U+WQKH>k@D<1zlUuU*vnb}RV;3sbaXTMA}=9rLcyp(~#_??mcoiFG3fchCcUHx3v IIVCg!004j{5C8xG literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___horse_armor.png b/src/main/resources/mmdlib_templates/textures/item/__material___horse_armor.png new file mode 100644 index 0000000000000000000000000000000000000000..297814611a1aa1d97f3c0639ffd6bc9769e24f16 GIT binary patch literal 298 zcmV+_0oDGAP)nQK_m5wDx_Ee+inV0kv&g9LMoDNs=U}>l$$!GcZk4Y}@vpgdl=F&x7MQAUMl1 z6h-l#fMB}8JkOA2nTeKVK@bE#0>dyMNfIb!Swav5(4L|wuxuzVCxd;l8=$1svqGzM}(KYXATM07*qoM6N<$g5j8bQ2+n{ literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___ingot.png b/src/main/resources/mmdlib_templates/textures/item/__material___ingot.png new file mode 100644 index 0000000000000000000000000000000000000000..442efe9129a7b84f3878dffc108a10880ca9d8e4 GIT binary patch literal 357 zcmV-r0h<1aP)-8FjVc>i|<8(T4FJ&a4375+SP17Jr5(GiO5H28kW;C8!l=qv<>!-3EH{r;<9yWMiAVYk~coMqWCZL`_HvMk<6Q4|Il{`?9;LIT%y zaXcP}X#yBQQ}0g#+qPkvCg$@wWLf?mnv(oD>Ce>%xV>lNk0VvX00000NkvXXu0mjf DQth10 literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___leggings.png b/src/main/resources/mmdlib_templates/textures/item/__material___leggings.png new file mode 100644 index 0000000000000000000000000000000000000000..33e422d6b32e29dfb1820dfa7ac3e307267f6bb3 GIT binary patch literal 287 zcmV+)0pR|LP)57G;KE;hJm(iktE3hdheaQd- literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___nugget.png b/src/main/resources/mmdlib_templates/textures/item/__material___nugget.png new file mode 100644 index 0000000000000000000000000000000000000000..c541b4cabdafa0accd8e1874fa738ba51f1ebb95 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`9-c0aAr-fh6C_wI91{{A)Jv#& ze_k8C{a-UfM_EC^5C2V!24@v&etby0#N%k5@bS^nCp(*TZcpT8$W%LBwn~D*xFzST cG6w_0oJ^v=0%o5nrDW7P3_c+Jy#MyZ_Kk)fT?r66@d?Wqg9A_`iY~4A<7ow82ZJxWV z6}3$Na_?dk^V{tzP8=FbuN{#PbULtlvzMm#rmIglIXy~r7Cu-lc1&W)lxy*QEcdE= l49~G%X|NEBsZXfSt8+6<_@x!F=p@iJ44$rjF6*2UngCc}NUZ<> literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___pickaxe.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___pickaxe.untinted.png new file mode 100644 index 0000000000000000000000000000000000000000..8e89c5ad6c439483adb93339653bba022476e2a8 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`CY~;iAr-fh6C^4RT=w}=-!}hF z{U+WQKH>k@D<1zlUuU*vnb}RV;3sbaXTMA}=9rLcyp(~#_??mcoiFG3fchCcUHx3v IIVCg!004j{5C8xG literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___powder_dirty.png b/src/main/resources/mmdlib_templates/textures/item/__material___powder_dirty.png new file mode 100644 index 0000000000000000000000000000000000000000..cccfc3b87f9b113d777a0c6801003d613aa728eb GIT binary patch literal 379 zcmV->0fhdEP)6oq{OEz~Fo{ue5ORj?Hs8zHEbq67qOL`95f=L?7kLaH=D3w;M4Azfa;BCT!e zPi9(O+1+Z{1BYqOd~@%C2?YMP*6TH!&4yoHuGi~0j)U*}OsCVIK{}ldLg(|D<#LH> znp7&4??Kw_Hnwf^cs#Hyi^*id@pxpv-%~1;K0}JdBG>B`*LCqckIUu4VzHpt>oFRQ zn9XLS)9Lq+d_K>7K9^(PZZ{b|ole~EcLsw2hr@wJqd_*CB^HalLK2At&1O@Ece@>{ z)r#$QD?8A2o#AjOS16au1cSj>Kq{3&Od*0ohG8%skI^)ZZnsOn->21Tp{gp8NaXz* z0+LK7DHIA+t5szopr0H&NSzyJUM literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___scythe.png b/src/main/resources/mmdlib_templates/textures/item/__material___scythe.png new file mode 100644 index 0000000000000000000000000000000000000000..731832cc071ed1a2317c62dfed17d873b6ed8111 GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`*`6+rAr-fh6BdY0m@?&0f1l?i z9_H)oEf zRvmqXc18|i6NYG=m>mTVMkELVc}NsRoaf25Z8(l24+z7MuIrel2~}0GEbBcaNfP#bfAodPFbo{W z@g9<v~LV+eWH|d7crlJkM#GhQ9A{T^Ffm xdY(sFmeh5P@B0{rA>WFoX^7Ev{adZSof}IedE2ZI)NTL(002ovPDHLkV1mR;X%qke literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___shears.png b/src/main/resources/mmdlib_templates/textures/item/__material___shears.png new file mode 100644 index 0000000000000000000000000000000000000000..0e98a935c1dd9c37466efabd12d9988f2096a12a GIT binary patch literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`>7Fi*Ar-ftPPXP_P~>p2ov}qs z&avuAOOf4`aS8UgY(<{g+rPhNG0#cv=&0}E zU9IIbUt-;xu(hI==`H1Z9|v{WduG3Na1nXkG&AwrTI)&%3%k(yeup1K0Ig*3boFyt I=akR{0J5Mt8vp zNj5%tJD*vK24}bD-DS#VV_d}RaGht7rx632#w8wRk<7Nr_CygLcLs*j#erMTI5(XK Pn#17f>gTe~DWM4f->4z7 literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___shovel.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___shovel.untinted.png new file mode 100644 index 0000000000000000000000000000000000000000..893e8985498970a54a014dab5fcc4fc1f3c07ac8 GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`2A(dCAr-fh6C_xjSt@3A{#))* zoWAQS&I_$2}XwR+!_K^3pcs|^)h(6`njxgN@xNA D$Dkk` literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___smallblend.png b/src/main/resources/mmdlib_templates/textures/item/__material___smallblend.png new file mode 100644 index 0000000000000000000000000000000000000000..d0166898d7017a49b1e0d1fe9c467e18c562ac02 GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`(Vi}jAr-gYPTI}MpeW$-Ho*I$ z=?44oY=JXt4l>;c-Ky_?aP_zEacod21Vj xxVDBpoAYGO^lf1l&%?5NCQcFhE-3$r;ij_f?Hx6&1wd;UJYD@<);T3K0RZxoHvRwr literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___smallpowder.png b/src/main/resources/mmdlib_templates/textures/item/__material___smallpowder.png new file mode 100644 index 0000000000000000000000000000000000000000..c894f53e0e58007c35fd8cfbb5cc3668fe34622a GIT binary patch literal 152 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`@t!V@Ar-gYUQ`rhP!w?uJf)E7 zpefQfbG;&qvC0D0SqvOP7akbhPAC^`dVJs1f930fWS(bp{z$FgT5#@SwffR8x=Cx7 zPH{Rnv;DiAV5R)UsN^2S7KiNKC~ZTxcPp3QIy~9P%i`CpUZ7PBp00i_>zopr00H7R AJOBUy literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___sword.png b/src/main/resources/mmdlib_templates/textures/item/__material___sword.png new file mode 100644 index 0000000000000000000000000000000000000000..f232f3b53622c2e3902d7a8ff7891836af8c8f94 GIT binary patch literal 253 zcmV{5CHQ`&d{FJKoBrG*e1FA)(7L72}3K3IHyTn#LNu=8ei6GabB zQ4~ni)R$P?G!3?G!!QhT0LABdVwxs6=VSoJrZdIsy2`|CrXypEoo|cdI0ozbmgl*T zJC0-6nPROqk|gm>RaIf%cPz_-<2ay{3dbg-?|amB4W4u__Y%;ggbc!`7ZV^Bos9`%5~;fQJ(@nlhwXj)HX%h00000NkvXXu0mjf Dg?42( literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___sword.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___sword.untinted.png new file mode 100644 index 0000000000000000000000000000000000000000..6b5df57f983ded5291532cf7b411d84faf60ddd9 GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`TAnVBAr-fh6C_v{C;VXaH@^5^ xvaaa=$!m9JuKO47+&9~X*}-h`+DZpT2FWn)=8F4oG=aJpJYD@<);T3K0RSI&9_Ii6 literal 0 HcmV?d00001 diff --git a/src/main/resources/mmdlib_templates/textures/models/armor/__material___layer_1.png b/src/main/resources/mmdlib_templates/textures/models/armor/__material___layer_1.png new file mode 100644 index 0000000000000000000000000000000000000000..fdeea25631f4fefec5304e0c4dc84a3953ac21e0 GIT binary patch literal 1126 zcmV-s1eyDZP)6hV?X>4T z`ym$lrae79xnwfw*4EbC^767V$o%@S+}yX;$rjOAa*z$27PaD&sD2c8v$cs z0BAOw?%?3SIslZ*Ws`!10O0-o-8e4pZ*Ol~Pb3oa4?w9@3J9m~?(Q1*85cqV>h-!i zJUnzX*#J8476JeWm)>_eoxlL{ZgX>U{sH*>{4`FhqxYFi#?@-I`EY|fJ3Hpz)iHXw zH~c=_USD6g4)OVXKA7inIRgPn;61%iDA>r}R4U~vm5SL40OLL1#eVo_MGgSO$j77C z5PfrV<65nj5vkv00I~iuf+9@&T)BZO)# zgdpa6-rCv<#2IM-ObF|CyYBhm(U0+}Sl`AN`R^9-Iv|dD#h%u)S!(NP?_CGv4SZ$J#K#H)M zQ4G-c2ZMn{qoGIuF+GQK5kGrG6iJK#LQx>UA-WKQJ%Iq8S$}zX>CVs3ZI9MNJV_OE z5!mbXY))4B_V(sZPfr6tkuJUqNB|g0zy%R-4FcdMV(e#!Bu2i3_!59h^!t5F#Q25^ zt^mLsai$8TOq9m6Y!1mAKyWUq1Hs%!su)VH!J$f%rN1Ij={3aZcP^`Rn6(h&*AcW= zDiccC8#K`uM$HL0FeS->P)59TY5QKgIM-mZKyzl$IpFl(eL3;TbO0xT3*d$%h>`Z1Ro2tIXLLm?c1OkCTAP@)y z0)as8(Qdb`-|yRKG_vV*YPl1j+wB_I?RJJ3xf5VK9^2`3a$}r30SJGH4~Ih|8Zu9~ zQmI(AT6MDX`P}ng|ZPvK2E&=B6Pi8oe%<001tvh|1w{Dr}68HPvj>qH2TztrJNGinfZgNh@BLszz zsJPv3-XiO`|V#wy z^uc&F`SIZOdifndABsT+0R(D9@{FJkLKJZZp#W*6Hv1Qf$W&a!5P|4`kh}>X%roi> zMqNPwNGH1tf&-$C{i;8lNky08{s0-J42B^4{r>Zt0wW4^i^RL694%Ni2U9!deF#FD z{CfeY3-TPP!N)^_Icb9@Az>% literal 0 HcmV?d00001 diff --git a/src/test/java/zone/moddev/mc/lib/common/builders/DefinitionLoaderTest.java b/src/test/java/zone/moddev/mc/lib/common/builders/DefinitionLoaderTest.java index 2d31d229..fcfc8c11 100644 --- a/src/test/java/zone/moddev/mc/lib/common/builders/DefinitionLoaderTest.java +++ b/src/test/java/zone/moddev/mc/lib/common/builders/DefinitionLoaderTest.java @@ -266,6 +266,132 @@ void loadsDefinitionsAcrossMods() throws Exception { } } + @Test + void plansCodeMaterialSources() throws Exception { + Path temporary = Files.createTempDirectory("mmdlib-sources"); + var loader = DefinitionLoaderTest.class.getClassLoader(); + try { + var root = temporary.resolve("alpha/mmdlib"); + write(root, "material/copper.json", "{\"items\":\"\",\"blocks\":\"\",\"has_fluid\":false}"); + var sources = List.of(new SourceRoot("alpha", root, loader)); + + var api = api(); + api.registerDefinitionSource("scripts", materials(codeMaterial("alpha:tin"))); + var loaded = DefinitionLoader.load(sources, api, mod -> false); + require( + loaded.materials().keySet().equals(java.util.Set.of(id("alpha:copper"), id("alpha:tin")))); + + // A source's module is installed before material files decode, so files can select it. + write(root, "material/stone.json", "{\"items\":\"\",\"blocks\":\"alpha:pillar\",\"has_fluid\":false}"); + MaterialExtensions.DefinitionSource pillarSource = + catalog -> { + catalog.registerBlockModule( + id("alpha:pillar"), + zone.moddev.mc.lib.common.modules.DataModules.Block.CODEC + .parse( + com.mojang.serialization.JsonOps.INSTANCE, + com.google.gson.JsonParser.parseString( + "{\"factory\":\"alpha:pillar_block\",\"template\":{}}")) + .getOrThrow()); + catalog.registerItemModule( + id("alpha:token"), + zone.moddev.mc.lib.common.modules.DataModules.Item.CODEC + .parse( + com.mojang.serialization.JsonOps.INSTANCE, + com.google.gson.JsonParser.parseString( + "{\"factory\":\"alpha:basic\",\"template\":{}}")) + .getOrThrow()); + return new MaterialExtensions.Definitions( + List.of(), + Map.of( + id("alpha:marble"), + new zone.moddev.mc.lib.common.types.StandaloneBlock( + id("alpha:pillar"), + zone.moddev.mc.lib.common.types.FactoryData.EMPTY, + java.util.Optional.empty(), + zone.moddev.mc.lib.common.types.MaterialRuntimeSettings.DEFAULT)), + Map.of( + id("alpha:coin"), + new zone.moddev.mc.lib.common.types.StandaloneItem( + id("alpha:token"), + zone.moddev.mc.lib.common.types.FactoryData.EMPTY, + java.util.Optional.empty(), + zone.moddev.mc.lib.common.types.MaterialRuntimeSettings.DEFAULT))); + }; + var withModule = api(); + withModule.registerDefinitionSource("scripts", pillarSource); + var moduled = DefinitionLoader.load(sources, withModule, mod -> false); + require( + moduled.materials().get(id("alpha:stone")).content().blockDefinitions() + .containsKey(id("alpha:stone_pillar"))); + require( + moduled.standalone().get(id("alpha:marble")).content().blockDefinitions() + .containsKey(id("alpha:marble"))); + require( + moduled.standaloneItems().get(id("alpha:coin")).content().itemDefinitions() + .containsKey(id("alpha:coin"))); + write(root, "standalone/block/marble.json", "{\"module\":\"alpha:pillar\"}"); + var again = api(); + again.registerDefinitionSource("scripts", pillarSource); + expectFailure( + () -> DefinitionLoader.load(sources, again, mod -> false), + "Duplicate standalone block alpha:marble", + "definition source 'scripts'"); + Files.delete(root.resolve("standalone/block/marble.json")); + Files.delete(root.resolve("material/stone.json")); + + var clash = api(); + clash.registerDefinitionSource("scripts", materials(codeMaterial("alpha:copper"))); + expectFailure( + () -> DefinitionLoader.load(sources, clash, mod -> false), + "Duplicate material alpha:copper", + "alpha/mmdlib/material/copper.json", + "definition source 'scripts'"); + + var twice = api(); + twice.registerDefinitionSource( + "scripts", materials(codeMaterial("alpha:tin"), codeMaterial("alpha:tin"))); + expectFailure( + () -> DefinitionLoader.load(List.of(), twice, mod -> false), + "Duplicate material alpha:tin"); + + var unnamed = api(); + unnamed.registerDefinitionSource( + "scripts", + materials( + new zone.moddev.mc.lib.common.types.MMDMaterial( + java.util.Optional.empty(), + java.util.Optional.empty(), + zone.moddev.mc.lib.common.types.MaterialProperties.DEFAULT, + nothing(), + Map.of(), + zone.moddev.mc.lib.common.types.MaterialRuntimeSettings.DEFAULT))); + expectFailure( + () -> DefinitionLoader.load(List.of(), unnamed, mod -> false), + "definition source 'scripts'", + "unnamed material"); + } finally { + try (var paths = Files.walk(temporary)) { + for (var path : paths.sorted(java.util.Comparator.reverseOrder()).toList()) + Files.delete(path); + } + } + } + + private static MaterialExtensions.DefinitionSource materials( + zone.moddev.mc.lib.common.types.MMDMaterial... materials) { + return api -> new MaterialExtensions.Definitions(List.of(materials), Map.of()); + } + + private static zone.moddev.mc.lib.common.types.MMDMaterial codeMaterial(String name) { + return MaterialBuilder.create().setName(name).setSelection(nothing()).build(); + } + + private static zone.moddev.mc.lib.common.types.MaterialSelection nothing() { + return new zone.moddev.mc.lib.common.types.MaterialSelection( + java.util.Set.of(), java.util.Set.of(), java.util.Set.of(), false); + } + private static MaterialExtensions api() { var api = new MaterialExtensions(false); api.events().registerEvent("test:probe", String.class); diff --git a/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java new file mode 100644 index 00000000..3a453714 --- /dev/null +++ b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java @@ -0,0 +1,247 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.imageio.ImageIO; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +import org.junit.jupiter.api.Test; + +import zone.moddev.mc.lib.api.MaterialExtensions; +import zone.moddev.mc.lib.common.builders.MaterialBuilder; +import zone.moddev.mc.lib.common.factories.MaterialFactory; + +final class ScriptMaterialResourcesTest { + private static final Path TEMPLATES = Path.of("src/main/resources/mmdlib_templates"); + + private static MaterialFactory.Result everything() { + var material = + MaterialBuilder.create() + .setName("mmdlib:cold_iron") + .setColor(0xFF4080C0) + .setHardness(8) + .setHarvestTags(java.util.Set.of(net.minecraft.resources.ResourceLocation.parse("c:test"))) + .build(); + return new MaterialFactory(MaterialExtensions.GLOBAL, mod -> true).create(material); + } + + @Test + void generatesCompleteAssetsForEveryPart() throws Exception { + var files = + ScriptMaterialResources.assets( + List.of(everything()), List.of(), List.of(), Map.of(), new MaterialTemplates(List.of(TEMPLATES)), TEMPLATES); + var textures = new HashSet(); + var models = new HashSet(); + for (var entry : files.entrySet()) { + var path = entry.getKey(); + require(!path.contains(MaterialTemplates.TOKEN), "token left in path " + path); + if (!path.endsWith(".json")) continue; + var text = new String(entry.getValue(), StandardCharsets.UTF_8); + require(!text.contains(MaterialTemplates.TOKEN), "token left in " + path); + collect(JsonParser.parseString(text), null, textures, models); + } + for (var model : models) + require(files.containsKey("assets/mmdlib/models/" + model + ".json"), "missing model " + model); + for (var texture : textures) + require( + files.containsKey("assets/mmdlib/textures/" + texture + ".png"), + "missing texture " + texture); + for (var path : + List.of( + "assets/mmdlib/blockstates/cold_iron_stairs.json", + "assets/mmdlib/blockstates/cold_iron_anvil.json", + "assets/mmdlib/models/item/cold_iron_bow.json", + "assets/mmdlib/models/item/cold_iron_molten_bucket.json", + "assets/mmdlib/textures/models/armor/cold_iron_layer_1.png", + "assets/mmdlib/textures/entity/horse/armor/horse_armor_cold_iron.png", + "assets/mmdlib/lang/en_us.json")) + require(files.containsKey(path), "missing " + path); + var lang = + JsonParser.parseString(new String(files.get("assets/mmdlib/lang/en_us.json"), StandardCharsets.UTF_8)) + .getAsJsonObject(); + require(lang.get("item.mmdlib.cold_iron_ingot").getAsString().equals("Cold Iron Ingot"), "ingot name"); + require(lang.get("block.mmdlib.cold_iron_block").getAsString().equals("Block of Cold Iron"), "block name"); + require(lang.get("fluid_type.mmdlib.cold_iron_molten").getAsString().equals("Molten Cold Iron"), "fluid name"); + require(!lang.has("item.mmdlib.cold_iron_stairs"), "block items use the block's name"); + } + + @Test + void colorizesGrayscaleAndKeepsUntintedPixels() throws Exception { + var gray = Files.readAllBytes(TEMPLATES.resolve("textures/block/__material___block.png")); + var image = ImageIO.read(new ByteArrayInputStream(ScriptMaterialResources.colorize(gray, Optional.empty(), 0xFFFF8000))); + var source = ImageIO.read(new ByteArrayInputStream(gray)); + int g = (source.getRGB(5, 5) >> 16) & 255, p = image.getRGB(5, 5); + require(((p >> 16) & 255) == g && ((p >> 8) & 255) == g * 0x80 / 255 && (p & 255) == 0, "multiplied"); + var tint = Files.readAllBytes(TEMPLATES.resolve("textures/item/__material___pickaxe.png")); + var keep = Files.readAllBytes(TEMPLATES.resolve("textures/item/__material___pickaxe.untinted.png")); + var handle = ImageIO.read(new ByteArrayInputStream(keep)); + var out = ImageIO.read(new ByteArrayInputStream(ScriptMaterialResources.colorize(tint, Optional.of(keep), 0xFF0000FF))); + boolean checked = false; + for (int y = 0; y < 16; y++) + for (int x = 0; x < 16; x++) + if ((handle.getRGB(x, y) >>> 24) == 255) { + require(out.getRGB(x, y) == handle.getRGB(x, y), "handle pixel recolored"); + checked = true; + } + require(checked, "pickaxe template has a handle"); + } + + @Test + void generatesLootAndTags() { + var files = ScriptMaterialResources.data(List.of(everything()), List.of(), List.of(), Map.of()); + var slab = text(files, "data/mmdlib/loot_table/blocks/cold_iron_double_slab.json"); + require(slab.contains("mmdlib:cold_iron_slab") && slab.contains("set_count"), "double slab drops slabs"); + require(text(files, "data/mmdlib/loot_table/blocks/cold_iron_door.json").contains("\"half\": \"lower\""), "door lower half"); + require(text(files, "data/minecraft/tags/block/needs_iron_tool.json").contains("mmdlib:cold_iron_block"), "harvest level"); + require(text(files, "data/c/tags/block/test.json").contains("mmdlib:cold_iron_ore"), "harvest tags"); + require(text(files, "data/minecraft/tags/block/walls.json").contains("mmdlib:cold_iron_wall"), "template tags"); + require(text(files, "data/minecraft/tags/item/arrows.json").contains("mmdlib:cold_iron_arrow"), "arrows"); + require(text(files, "data/c/tags/item/ingots/cold_iron.json").contains("mmdlib:cold_iron_ingot"), "c:ingots"); + require(text(files, "data/c/tags/fluid/molten_cold_iron.json").contains("mmdlib:flowing_cold_iron_molten"), "fluid tag"); + } + + @Test + void generatesStandaloneBlocksNamedAfterTheirId() { + var stairs = net.minecraft.resources.ResourceLocation.parse("mmdlib:stairs"); + var marble = + new zone.moddev.mc.lib.common.types.StandaloneBlock( + stairs, + zone.moddev.mc.lib.common.types.FactoryData.EMPTY, + Optional.empty(), + zone.moddev.mc.lib.common.types.MaterialRuntimeSettings.DEFAULT) + .resolve( + net.minecraft.resources.ResourceLocation.parse("mmdlib:marble_stairs"), + MaterialExtensions.GLOBAL.blockModule(stairs)); + var files = + ScriptMaterialResources.assets( + List.of(), List.of(marble), List.of(), Map.of(), new MaterialTemplates(List.of(TEMPLATES)), TEMPLATES); + require(files.containsKey("assets/mmdlib/blockstates/marble_stairs.json"), "blockstate"); + require(files.containsKey("assets/mmdlib/models/block/marble_stairs_inner.json"), "inner model"); + require(files.containsKey("assets/mmdlib/models/item/marble_stairs.json"), "item model"); + require( + text(files, "assets/mmdlib/models/block/marble_stairs.json").contains("mmdlib:block/marble_stairs\""), + "uses its own texture"); + require(files.keySet().stream().noneMatch(path -> path.endsWith(".png")), "no generated textures"); + require(text(files, "assets/mmdlib/lang/en_us.json").contains("\"Marble Stairs\""), "name"); + var data = ScriptMaterialResources.data(List.of(), List.of(marble), List.of(), Map.of()); + require(data.containsKey("data/mmdlib/loot_table/blocks/marble_stairs.json"), "loot"); + } + + @Test + void materialPartsFromNewModulesUseTheirFactoryTemplate() { + var api = new MaterialExtensions(); + var stairs = api.blockModule(net.minecraft.resources.ResourceLocation.parse("mmdlib:stairs")); + api.registerBlockModule(net.minecraft.resources.ResourceLocation.parse("mmdlib:test_steps"), stairs); + var material = + MaterialBuilder.create() + .setName("mmdlib:tin") + .setSelection( + new zone.moddev.mc.lib.common.types.MaterialSelection( + java.util.Set.of(), + java.util.Set.of(api.snapshot().parts().resolve("test_steps").getOrThrow()), + java.util.Set.of(), + false)) + .build(); + var result = new MaterialFactory(api, mod -> true).create(material); + var files = + ScriptMaterialResources.assets( + List.of(result), List.of(), List.of(), Map.of(), new MaterialTemplates(List.of(TEMPLATES)), TEMPLATES); + require(files.containsKey("assets/mmdlib/blockstates/tin_test_steps.json"), "blockstate"); + require(files.containsKey("assets/mmdlib/models/block/tin_test_steps_outer.json"), "outer model"); + require(files.containsKey("assets/mmdlib/textures/block/tin_block.png"), "material texture"); + require(text(files, "assets/mmdlib/lang/en_us.json").contains("\"Tin Test Steps\""), "name"); + } + + private static MaterialFactory.Result item(String id, String module) { + var rl = net.minecraft.resources.ResourceLocation.parse(module); + return new zone.moddev.mc.lib.common.types.StandaloneItem( + rl, + zone.moddev.mc.lib.common.types.FactoryData.EMPTY, + Optional.empty(), + zone.moddev.mc.lib.common.types.MaterialRuntimeSettings.DEFAULT) + .resolve(net.minecraft.resources.ResourceLocation.parse(id), MaterialExtensions.GLOBAL.itemModule(rl)); + } + + @Test + void generatesStandaloneItemsNamedAfterTheirId() { + var wrench = item("mmdlib:wrench", "mmdlib:ingot"); + var longbow = item("mmdlib:longbow", "mmdlib:bow"); + var files = + ScriptMaterialResources.assets( + List.of(), List.of(), List.of(wrench, longbow), Map.of(), + new MaterialTemplates(List.of(TEMPLATES)), TEMPLATES); + require(text(files, "assets/mmdlib/models/item/wrench.json").contains("mmdlib:item/wrench\""), "plain item texture"); + require(text(files, "assets/mmdlib/models/item/longbow.json").contains("mmdlib:item/longbow_pulling_0"), "bow overrides"); + require(files.containsKey("assets/mmdlib/models/item/longbow_pulling_2.json"), "pulling model"); + require(text(files, "assets/mmdlib/models/item/longbow.json").contains("mmdlib:item/longbow_standby"), "bow texture"); + require(files.keySet().stream().noneMatch(path -> path.endsWith(".png")), "no generated textures"); + require(text(files, "assets/mmdlib/lang/en_us.json").contains("\"item.mmdlib.wrench\": \"Wrench\""), "name"); + var wrenchId = net.minecraft.resources.ResourceLocation.parse("mmdlib:wrench"); + var data = + ScriptMaterialResources.data( + List.of(), List.of(), List.of(wrench), + Map.of(wrenchId, java.util.Set.of(net.minecraft.resources.ResourceLocation.parse("c:tools/wrench")))); + require(text(data, "data/c/tags/item/tools/wrench.json").contains("mmdlib:wrench"), "item tag"); + } + + @Test + void materialPartsFromItemModulesUseTheModuleTheyCopy() { + var api = new MaterialExtensions(); + var ingot = net.minecraft.resources.ResourceLocation.parse("mmdlib:ingot"); + var widget = net.minecraft.resources.ResourceLocation.parse("mmdlib:test_widget"); + api.registerItemModule(widget, api.itemModule(ingot)); + var material = + MaterialBuilder.create() + .setName("mmdlib:tin") + .setSelection( + new zone.moddev.mc.lib.common.types.MaterialSelection( + java.util.Set.of(api.snapshot().parts().resolve("test_widget").getOrThrow()), + java.util.Set.of(), java.util.Set.of(), false)) + .build(); + var result = new MaterialFactory(api, mod -> true).create(material); + var files = + ScriptMaterialResources.assets( + List.of(result), List.of(), List.of(), Map.of(widget, ingot), + new MaterialTemplates(List.of(TEMPLATES)), TEMPLATES); + require(files.containsKey("assets/mmdlib/models/item/tin_test_widget.json"), "model from the copied module"); + require(files.containsKey("assets/mmdlib/textures/item/tin_test_widget.png"), "colored texture"); + var data = + ScriptMaterialResources.data( + List.of(result), List.of(), List.of(), + Map.of(widget, java.util.Set.of(net.minecraft.resources.ResourceLocation.parse("c:widgets")))); + require(text(data, "data/c/tags/item/widgets.json").contains("mmdlib:tin_test_widget"), "module tags"); + } + + private static String text(Map files, String path) { + require(files.containsKey(path), "missing " + path); + return new String(files.get(path), StandardCharsets.UTF_8); + } + + private static void collect(JsonElement element, String key, java.util.Set textures, java.util.Set models) { + if (element.isJsonObject()) + element.getAsJsonObject().entrySet().forEach(e -> collect(e.getValue(), e.getKey(), textures, models)); + else if (element.isJsonArray()) element.getAsJsonArray().forEach(e -> collect(e, key, textures, models)); + else if (element.getAsJsonPrimitive().isString() + && element.getAsString().startsWith("mmdlib:") + && element.getAsString().contains("cold_iron") + && !"fluid".equals(key)) { + // Generated references to this material's models and textures, such as mmdlib:block/cold_iron_block. + var value = element.getAsString().substring("mmdlib:".length()); + if ("model".equals(key) || "parent".equals(key)) models.add(value); + else textures.add(value); + } + } + + private static void require(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } +} From 33972904be4ed73460b70f566a2bd4742f8cb5e0 Mon Sep 17 00:00:00 2001 From: Daniel Hazelton Date: Sat, 7 Nov 2026 13:32:57 -0500 Subject: [PATCH 3/9] Document CraftTweaker integration branch changes --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..dbf83f33 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +## Unreleased — CraftTweaker integration + +Changes on `crafttweaker-integration` since `master-2.0-1.21.1-neo`. + +- Add optional CraftTweaker support for defining materials, standalone blocks and + items, and reusable block and item modules in ZenScript. +- Add the `#loader mmdlib` startup loader. Definitions run before registry events + and use MMDLib's existing validation and registration path. Changes require a + restart; ordinary CraftTweaker scripts remain the place for recipes. +- Generate blockstates, models, English names, loot tables, and tags for script + content. Supply material texture templates tinted by material color, and load + `scripts/mmdlibdata` as an override pack for custom resources. +- Expose `MaterialExtensions.registerDefinitionSource` for code-built definitions. + Definition sources can register modules before material files are decoded. +- Add the material runtime `tinted` setting so precolored textures can opt out of + material tinting. +- Keep CraftTweaker optional, with its integration isolated under + `compat/CraftTweaker` and initialized only when CraftTweaker is installed. +- Document the scripting API, generated resources, startup ordering, and current + limitations in [CraftTweaker definitions](docs/crafttweaker.md) and + [definition loading](docs/definition-loading.md). From 18624283d44e5c3d313f4e21efb28838df0cf85e Mon Sep 17 00:00:00 2001 From: Daniel Hazelton Date: Sat, 7 Nov 2026 13:33:55 -0500 Subject: [PATCH 4/9] Add CraftTweaker behavior hooks and directional redstone Expose script callbacks for item use, melee hits, block breaking, placement, stepping, random ticks, and clicks through shared material event contracts. Support directional redstone inputs and outputs, delayed updates, comparator readings, and facing-aware generated models. Add runtime smoke fixtures, CraftTweaker game-test scripts, focused unit coverage, and API documentation. --- docs/crafttweaker.md | 182 +++++++++- docs/method-events.md | 50 ++- docs/runtime-behaviors.md | 20 +- gradle/verification.gradle | 15 +- scripts/generate_material_templates.py | 9 + .../mc/lib/api/MaterialEventContexts.java | 228 ++++++++++++ .../moddev/mc/lib/api/MaterialEventTypes.java | 76 ++++ .../mc/lib/common/runtime/BlockHooks.java | 36 ++ .../lib/common/runtime/ItemEventBridge.java | 118 +++++++ .../mc/lib/common/runtime/RedstoneBlock.java | 231 ++++++++++-- .../mc/lib/common/runtime/StandardBlocks.java | 30 ++ .../runtime/StandardImplementations.java | 8 +- .../mc/lib/common/runtime/StandardItems.java | 55 ++- .../CraftTweaker/ScriptBlockBuilder.java | 11 + .../CraftTweaker/ScriptBlockContext.java | 109 ++++++ .../CraftTweaker/ScriptBlockEventContext.java | 31 ++ .../CraftTweaker/ScriptBlockEventHandler.java | 14 + .../CraftTweaker/ScriptBlockModule.java | 11 + .../CraftTweaker/ScriptBlockProperties.java | 330 +++++++++++++++++- .../CraftTweaker/ScriptBreakContext.java | 82 +++++ .../CraftTweaker/ScriptBreakHandler.java | 14 + .../CraftTweaker/ScriptClickContext.java | 42 +++ .../CraftTweaker/ScriptClickHandler.java | 14 + .../ScriptDefinitionProperties.java | 36 ++ .../compat/CraftTweaker/ScriptHitContext.java | 43 +++ .../compat/CraftTweaker/ScriptHitHandler.java | 14 + .../CraftTweaker/ScriptItemBuilder.java | 5 + .../CraftTweaker/ScriptItemClickContext.java | 82 +++++ .../CraftTweaker/ScriptItemClickHandler.java | 14 + .../compat/CraftTweaker/ScriptItemModule.java | 5 + .../CraftTweaker/ScriptItemProperties.java | 117 ++++++- .../CraftTweaker/ScriptItemUseContext.java | 76 ++++ .../CraftTweaker/ScriptItemUseHandler.java | 14 + .../CraftTweaker/ScriptMaterialResources.java | 15 +- .../CraftTweaker/ScriptRedstoneContext.java | 37 ++ .../CraftTweaker/ScriptRedstoneHandler.java | 14 + .../__material___block_facing.json | 19 + .../block/__material___block_facing.json | 8 + .../item/__material___block_facing.json | 3 + .../moddev/mc/lib/smoke/ContentSmoke.java | 14 +- .../zone/moddev/mc/lib/smoke/HookSmoke.java | 169 +++++++++ .../moddev/mc/lib/smoke/MMDLibGameTests.java | 40 +++ .../moddev/mc/lib/smoke/RedstoneSmoke.java | 68 ++++ .../moddev/mc/lib/smoke/ScriptHookSmoke.java | 130 +++++++ .../mmdlib/extension/test_hooks.json | 58 +++ .../mmdlib/extension/test_redstone.json | 10 + .../standalone/block/test_hook_block.json | 12 + .../standalone/block/test_nand_gate.json | 14 + .../standalone/item/test_hook_tool.json | 14 + src/runtimeTest/scripts/mmdlib_hooks.zs | 54 +++ .../common/builders/RuntimeBehaviorTest.java | 33 ++ .../ScriptMaterialResourcesTest.java | 26 ++ 52 files changed, 2817 insertions(+), 43 deletions(-) create mode 100644 src/main/java/zone/moddev/mc/lib/common/runtime/BlockHooks.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockContext.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockEventContext.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockEventHandler.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBreakContext.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBreakHandler.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptClickContext.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptClickHandler.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptHitContext.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptHitHandler.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemClickContext.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemClickHandler.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemUseContext.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemUseHandler.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptRedstoneContext.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptRedstoneHandler.java create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___block_facing.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___block_facing.json create mode 100644 src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___block_facing.json create mode 100644 src/runtimeTest/java/zone/moddev/mc/lib/smoke/HookSmoke.java create mode 100644 src/runtimeTest/java/zone/moddev/mc/lib/smoke/RedstoneSmoke.java create mode 100644 src/runtimeTest/java/zone/moddev/mc/lib/smoke/ScriptHookSmoke.java create mode 100644 src/runtimeTest/resources/mmdlib/extension/test_hooks.json create mode 100644 src/runtimeTest/resources/mmdlib/extension/test_redstone.json create mode 100644 src/runtimeTest/resources/mmdlib/standalone/block/test_hook_block.json create mode 100644 src/runtimeTest/resources/mmdlib/standalone/block/test_nand_gate.json create mode 100644 src/runtimeTest/resources/mmdlib/standalone/item/test_hook_tool.json create mode 100644 src/runtimeTest/scripts/mmdlib_hooks.zs diff --git a/docs/crafttweaker.md b/docs/crafttweaker.md index 1a64ec41..2c859059 100644 --- a/docs/crafttweaker.md +++ b/docs/crafttweaker.md @@ -193,6 +193,8 @@ longbow.build(); | `stackSize` | int | 1 to 99 | | `durability` | int | uses before breaking; 0 for none. Setting it also sets `stackSize` to 1 | | `fireResistant` | bool | survives fire and lava | +| `useDuration` | int | ticks of use when right-clicked, like eating; 0 (default) for none. See [Hooks](#using-an-item-over-time) | +| `useAnimation` | string | how it is held while used: `none`, `eat`, `drink`, `bow`... | | `container` | item ID | left in the crafting grid after use, such as `minecraft:bucket`; `""` for nothing | | `factoryData` | map | settings for the module's Java implementation | | `tags`, `events` | string[], read-only | | @@ -229,6 +231,180 @@ tin.build(); A module's tags go to every material part built from it. The part's art comes from the template of the module it was copied from, in the material color. +## Redstone + +A block built by `mmdlib:block` (the default module) can react to redstone, emit it, +and run script functions. These properties write the block's +`factory_data.redstone` (see [runtime behaviors](runtime-behaviors.md#redstone)): + +| Property | Type | Meaning | +| --- | --- | --- | +| `redstoneInput` | bool | read the power reaching the block; set by `onRedstoneChanged` | +| `redstoneOutput` | bool | emit a power that handlers set with `context.output` | +| `strongPower` | bool | also power through the touching block, like a lever | +| `directional` | bool | face the way the placer looked; sides can then be `front`, `back`, `left`, `right` | +| `redstoneDelay` | int | ticks before `onRedstoneChanged` runs; 1 by default, 0 for at once | +| `comparator` | string | what a comparator reads: `"input"`, `"output"`, or `""` | + +`addOutputFaces`/`removeOutputFaces` choose the sides the output leaves from, and +`addInputFaces`/`removeInputFaces` the sides the block reads. Both default to every +side, and `remove...("all")` clears them. A block that reads only some sides runs its +handler when any of them changes. + +Two handlers take script functions: + +* `onRedstoneChanged((ctx as RedstoneContext) => {...})` runs on the server when the + power reaching the block changes, after `redstoneDelay` ticks. +* `onRightClick((ctx as ClickContext) => {...})` runs when a player right-clicks the + block, on both sides. The click is used up, so the held item does nothing. + +Both contexts have: + +* `input(side)`: the power reaching one side; +* `output`: the power emitted, read and set, clamped to 0-15 and ignored on the client; +* `facing`, `x`, `y`, `z`, and `client`. + +`RedstoneContext` adds `power` (the strongest read input), `previous`, and +`powered`. `ClickContext` adds `player`, `sneaking`, and `face`. A handler that +throws is logged, and the game carries on. + +An analog lever: each click raises its output by one, wrapping after 15, and a +sneaking click lowers it. + +```zenscript +#loader mmdlib +import mods.mmdlib.BlockBuilder; +import mods.mmdlib.ClickContext; +import mods.mmdlib.RedstoneContext; + +var lever = new BlockBuilder("analog_lever"); +lever.redstoneOutput = true; +lever.strongPower = true; +lever.comparator = "output"; +lever.onRightClick((ctx as ClickContext) => { + if (ctx.sneaking) { + ctx.output = ctx.output - 1; + } else { + ctx.output = (ctx.output + 1) % 16; + } +}); +lever.build(); +``` + +A NAND gate that reads its left and right sides and powers only its front: + +```zenscript +var gate = new BlockBuilder("nand_gate"); +gate.directional = true; +gate.redstoneOutput = true; +gate.removeOutputFaces("all").addOutputFaces("front"); +gate.addInputFaces("left", "right"); +gate.onRedstoneChanged((ctx as RedstoneContext) => { + ctx.output = (ctx.input("left") > 0 && ctx.input("right") > 0) ? 0 : 15; +}); +gate.build(); +``` + +A directional block gets a front and a top texture as well as its sides: +`block/nand_gate.png`, `block/nand_gate_front.png`, and `block/nand_gate_top.png`. +The model turns with the block, so an arrow drawn on the top points the way it +faces. Handlers on a block module apply to every block built from it, material parts +included. + +## Hooks + +Script functions can react to what players do with blocks and items, much as a Java +subclass would. Each `onXxx` method binds a function to one of MMDLib's +[hook contracts](method-events.md#hook-contracts). Java code and JSON extension files +can bind the same contracts. Calling a method again replaces the function. A function +that throws is logged, and the game carries on. + +| Builder | Method | Context | Runs | +|---|---|---|---| +| item | `onRightClick` | `ItemClickContext`: `player`, `stack`, `target` (`air`, `block`, `entity`), `entity`, `x`/`y`/`z`, `face` | both sides | +| item | `onUseStart`, `onUseTick`, `onUseStop`, `onUseFinish` | `ItemUseContext`: `entity`, `stack`, `phase`, `duration`, `result`, `cancel()` | both sides | +| item | `onHit` | `HitContext`: `attacker`, `target`, `stack`, `damage` | server, after a melee hit | +| item | `onBreakBlock` | `BreakContext`: `player`, `tool`, `block`, `x`/`y`/`z`, `face`, `cancel()`, `mine(x, y, z)` | server, before the block goes | +| block | `onRightClick`, `onRedstoneChanged` | see [Redstone](#redstone) | | +| block | `onBroken` | `BreakContext` | server, before the block goes | +| block | `onPlaced`, `onSteppedOn`, `onRandomTick` | `BlockEventContext`: the block context plus `entity` | placed: server; stepped on: both sides; random tick: server | + +Entities, players and stacks are the real game objects as CraftTweaker exposes them, +so their CraftTweaker methods work: `ctx.target.setRemainingFireTicks(100)`, +`ctx.player.isShiftKeyDown()`, `ctx.stack.withAmount(2)`. + +Handlers that run on both sides should check `ctx.client` before changing the world. +`duration` can only be set in `onUseStart`, `result` only in `onUseFinish`, and +`cancel()` only works while use starts or ticks. + +`onSteppedOn` and `onRandomTick` need a block built by `mmdlib:block`, because only +that block's class fires them; NeoForge has no event for either. Using them on stairs +or a door fails at `build()` and says so. `onRandomTick` turns on `randomTicks`. Every +other hook works on any block or item. + +### Using an item over time + +The use handlers need an item that is used over time. Food and bows already are. A +plain item becomes one with `useDuration` (ticks) and `useAnimation` (`none`, `eat`, +`drink`, `block`, `bow`, `spear`, `crossbow`, `spyglass`, `toot_horn`, `brush`): + +```zenscript +#loader mmdlib +import mods.mmdlib.ItemBuilder; +import mods.mmdlib.ItemUseContext; + +var charm = new ItemBuilder("ember_charm"); +charm.useDuration = 30; +charm.useAnimation = "bow"; +charm.onUseFinish((ctx as ItemUseContext) => { + if (!ctx.client) ctx.entity.setRemainingFireTicks(0); +}); +charm.build(); +``` + +### Weapon effects + +```zenscript +import mods.mmdlib.HitContext; + +var blade = new ItemBuilder("ember_blade", "mmdlib:sword"); +blade.onHit((ctx as HitContext) => { + ctx.target.setRemainingFireTicks(100); +}); +blade.build(); +``` + +### A 3×3 hammer + +`mine(x, y, z)` breaks a block as the player would, with drops, experience, tool wear +and protection mods' checks. It skips air, unbreakable blocks, blocks the tool can't +harvest, and the block already being broken, and its breaks don't run the hammer's +handler again. `face` tells which plane to clear: + +```zenscript +import mods.mmdlib.BreakContext; + +var hammer = new ItemBuilder("hammer", "mmdlib:pickaxe"); +hammer.onBreakBlock((ctx as BreakContext) => { + if (ctx.player.isShiftKeyDown()) return; + for a in -1 .. 2 { + for b in -1 .. 2 { + if (ctx.face == "up" || ctx.face == "down") { + ctx.mine(ctx.x + a, ctx.y, ctx.z + b); + } else if (ctx.face == "north" || ctx.face == "south") { + ctx.mine(ctx.x + a, ctx.y + b, ctx.z); + } else { + ctx.mine(ctx.x, ctx.y + a, ctx.z + b); + } + } + } +}); +hammer.build(); +``` + +Sneaking mines a single block. The game tests run this hammer +(`src/runtimeTest/scripts/mmdlib_hooks.zs`). + ## Generated resources Script materials need no resource pack, and script blocks and items need only their @@ -285,7 +461,11 @@ items, blocks, and their resources are created at startup on both sides. * Material events, runtime settings (sound, map color, block set type), and parent materials. These need the material file format for now. -* Behavior written in script: event handlers as script functions. This is next. +* Hooks without a contract yet: items ticking in an inventory, crafting and + smelting, tossing and picking up, tooltips, burn time, entities inside a block, + falling on a block. Each needs a contract like the others. The class-only ones + (inventory tick, entity inside, fall) would work on MMDLib's own classes: every + item, and the plain block. * Item template fields that are kept but not applied to registered items yet: lore, food, use time and animation, attribute modifiers, and tool actions. Scripts do not expose them until they work. diff --git a/docs/method-events.md b/docs/method-events.md index d42ce36a..53172be3 100644 --- a/docs/method-events.md +++ b/docs/method-events.md @@ -40,6 +40,10 @@ IDs must be unique across loaded files and code-registered callbacks. | `projectile-damage` | `Projectile projectile, LivingEntity target, ItemStack ammo, DamageContainer damage` | Yes | | `projectile-hit` | `Projectile projectile, LivingEntity target, ItemStack ammo` | Yes | | `redstone-changed` | `MaterialEventContexts.RedstoneChange change` | Yes | +| `item-use-start`, `item-use-tick`, `item-use-stop`, `item-use-finish` | `MaterialEventContexts.ItemUse use` | Yes | +| `item-hit` | `MaterialEventContexts.ItemHit hit` | Yes | +| `item-break-block`, `block-broken` | `MaterialEventContexts.BlockBreak broken` | Yes | +| `block-placed`, `block-stepped-on`, `block-random-tick` | `MaterialEventContexts.BlockAction action` | Yes | `ItemEventBridge` dispatches every built-in contract; see [item and material bindings](#item-and-material-bindings) for the NeoForge events and sides. @@ -223,7 +227,8 @@ Item templates bind handlers by event name in `events`: for any registered item or block whose ID has a loaded MMDLib definition (including custom types registered through the extension API), whatever its Java class. The item definition's binding wins; otherwise the item's material binding applies. Click and tick events fire on both logical sides; world-changing handlers -must check the side. It never cancels the NeoForge event. +must check the side. It cancels the NeoForge event only when a handler asks: a +consumed block click, or a cancelled use or break (see [hook contracts](#hook-contracts)). | NeoForge event | Binding | Target | |---|---|---| @@ -238,6 +243,10 @@ must check the side. It never cancels the NeoForge event. | `PlayerTickEvent.Post` (worn armor and both hands) | `player-tick` | `Player` | | `LivingIncomingDamageEvent` (server, projectile direct source) | `projectile-damage` | `ProjectileDamage` | | `LivingDamageEvent.Post` (server, projectile direct source) | `projectile-hit` | `ProjectileHit` | +| `LivingEntityUseItemEvent.Start` / `.Tick` / `.Stop` / `.Finish` (both sides) | `item-use-start` / `-tick` / `-stop` / `-finish` | `ItemUse` | +| `LivingDamageEvent.Post` (server, the attacker is the direct source: melee) | `item-hit`, by the attacker's main-hand item | `ItemHit` | +| `BlockEvent.BreakEvent` (server) | `item-break-block` by the main-hand item, then `block-broken` by the block | `BlockBreak` | +| `BlockEvent.EntityPlaceEvent` (server) | `block-placed` | `BlockAction` | `player-tick` runs each bound callback once per tick however many worn or held items share it: a material binding is a per-material effect, and its handler can count the @@ -251,6 +260,45 @@ Vanilla follows an unconsumed block click with an item use, so a right-click on block can dispatch a `BlockTarget` and then an `AirTarget`. Item bindings are validated against the callback catalog when materials load. +## Hook contracts + +These contracts let content react the way a Java subclass would, without one. Their +handlers take the context alone, and some contexts carry a result that is written back +to the game after the handler returns. + +| Contract | Context | Results | +|---|---|---| +| `item-use-start` | `ItemUse` (phase `START`, total duration) | `setDuration`, `cancel` | +| `item-use-tick` | `ItemUse` (ticks left) | `cancel` | +| `item-use-stop` | `ItemUse` (ticks left; released early) | none | +| `item-use-finish` | `ItemUse` (`result` is the stack left in hand) | `setResult` | +| `item-hit` | `ItemHit`: attacker, target, stack, damage dealt | none | +| `item-break-block`, `block-broken` | `BlockBreak`: player, level, position, state, tool, face mined | `cancel`; `mine(pos)` | +| `block-placed` | `BlockAction` with the placing entity | none | +| `block-stepped-on` | `BlockAction` with the entity | none | +| `block-random-tick` | `BlockAction` without an entity | none | + +The use events need an item that is used over time. Food, bows and the like already +are. A plain item (`mmdlib:basic_item`) is used over time when its template sets +`delayed_use`: + +```json +{"template": {"delayed_use": {"duration": 20, "animation": "bow"}, + "events": {"item-use-finish": "example:charge_finished"}}} +``` + +`BlockBreak.mine(pos)` breaks another block as the player would, with drops, +experience, tool wear and other mods' break checks. It skips air, unbreakable blocks, +blocks the tool cannot harvest, and the block already being broken, and its breaks +are not reported to `item-break-block` again. This is how a 3×3 tool works. `face()` +is the face the player is looking at, or null when the block is out of sight. + +**Where each hook works.** Every hook except two is fired from a NeoForge event, so it +works for any MMDLib item or block, whatever its class. `block-stepped-on` and +`block-random-tick` have no NeoForge event; only MMDLib's plain block (`mmdlib:block`, +with or without redstone) fires them, and `block-random-tick` also needs +`random_ticks` on. Binding them to another block form is allowed, but nothing fires. + ## Custom method signatures Mods can register additional contracts, with adapters from typed contexts to the diff --git a/docs/runtime-behaviors.md b/docs/runtime-behaviors.md index 25da7cb8..9fdd10f4 100644 --- a/docs/runtime-behaviors.md +++ b/docs/runtime-behaviors.md @@ -212,10 +212,26 @@ Only the block-state properties its settings use are added. | `strong` | `true` / `false` | Also power through the touching block, like a lever's attached face (default weak) | | `faces` | list of `down`, `up`, `north`, `south`, `west`, `east` | Faces that emit (default all) | | `comparator` | `0`-`15`, `"input"`, or `"output"` | Comparator reading; `"input"` needs `input`, `"output"` needs `output: "state"` | +| `facing` | `true` / `false` | The block faces the way its placer looked (a `facing` property, turned by structure rotation); `faces` and `inputs` may then name `front`, `back`, `left`, `right` | +| `inputs` | list of faces | Faces read (default all); needs `input`. Reading only some fires `redstone-changed` whenever any of them changes, even if the strongest stays the same | +| `delay` | `0`-`100` | Ticks between a neighbour change and reading the inputs (default 0, at once); needs `input` | + +A single-block gate reads its sides and powers its front: + +```json +"redstone": { "input": true, "output": "state", "facing": true, + "inputs": ["left", "right"], "faces": ["front"], "delay": 2 } +``` + +Its `redstone-changed` handler reads each side with `RedstoneChange.signal(face)`, +turning relative faces into world ones with `RedstoneBlock.toWorld(state, face)` +(`north` is the front, `west` the left), and sets its output with `setOutput`. Unknown keys and out-of-range values fail when the definition loads. A block that -both reads and emits can latch itself on through neighbouring dust; that is a wiring -choice for the author. Comparator readings from a block-entity getter wait for +both reads and emits on the same face can latch itself on through neighbouring dust; +keep `inputs` and `faces` apart to avoid it. With `delay` 0, handlers run inside the +neighbour update that caused them, so blocks wired into a loop recurse; a delay of 1 +or more advances such loops one step per scheduled tick, like repeaters. Comparator readings from a block-entity getter wait for block-entity runtime support. The native blocks still need the appropriate tags, blockstate models, loot tables, diff --git a/gradle/verification.gradle b/gradle/verification.gradle index b2252834..13b35360 100644 --- a/gradle/verification.gradle +++ b/gradle/verification.gradle @@ -38,8 +38,21 @@ neoForge { neoForge.runs.matching { it.name != 'gameTestServer' }.configureEach { loadedMods = [neoForge.mods.named(mod_id).get()] } +// Game tests also run the CraftTweaker integration: CraftTweaker is on this run's classpath only, +// and the scripts in src/runtimeTest/scripts are copied into the fresh game directory. +dependencies { + runtimeTestRuntimeOnly "com.blamejared.crafttweaker:CraftTweaker-neoforge-1.21.1:${crafttweaker_version}" +} def gameTestTask = tasks.named('runGameTestServer') -gameTestTask.configure { it.doFirst { project.delete(gameTestDir); gameTestDir.get().asFile.mkdirs() } } +gameTestTask.configure { + def scripts = file('src/runtimeTest/scripts') + inputs.dir(scripts) + it.doFirst { + project.delete(gameTestDir) + gameTestDir.get().asFile.mkdirs() + project.copy { from scripts; into gameTestDir.get().dir('scripts') } + } +} tasks.named('check') { dependsOn gameTestTask } // Unit tests in src/test: JUnit 5, no running game. `./gradlew test` (part of `check`). diff --git a/scripts/generate_material_templates.py b/scripts/generate_material_templates.py index cf3ef542..6b50f437 100644 --- a/scripts/generate_material_templates.py +++ b/scripts/generate_material_templates.py @@ -348,6 +348,15 @@ def rewrite(node): 'elements': [{'from': [0, 0, 0], 'to': [16, 16, 16], 'faces': faces('#base')}, {'from': [0, 0, 0], 'to': [16, 16, 16], 'faces': faces('#overlay')}]}) put('models/item/' + TOKEN + '_' + part, {'parent': T + '_' + part}) + # A directional block (redstone "facing"): front, side and top textures, turned to face the + # way it was placed. Used for standalone blocks; materials have no directional part. + put('blockstates/' + TOKEN + '_block_facing', + {'variants': {f'facing={d}': ({'model': T + '_block_facing', 'y': y} if y else {'model': T + '_block_facing'}) + for d, y in [('north', 0), ('east', 90), ('south', 180), ('west', 270)]}}) + put('models/block/' + TOKEN + '_block_facing', + {'parent': 'minecraft:block/orientable', + 'textures': {'front': T + '_block_facing_front', 'side': T + '_block', 'top': T + '_block_facing_top'}}) + put('models/item/' + TOKEN + '_block_facing', {'parent': T + '_block_facing'}) # The molten fluid: rendered by its fluid type, so the block itself has no model. put('blockstates/' + TOKEN + '_molten', {'variants': {'': {'model': 'minecraft:block/air'}}}) put('models/item/' + TOKEN + '_molten_bucket', diff --git a/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java b/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java index ec18c8e6..04d8d1fd 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java +++ b/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java @@ -125,6 +125,11 @@ public record RedstoneChange( public boolean powered() { return power > 0; } + + /** The power arriving at the block's {@code face} (a world direction) right now. */ + public int signal(net.minecraft.core.Direction face) { + return level.getSignal(position.relative(face), face); + } } /** Context of {@code block-right-click}: who clicked, and the click itself. */ @@ -256,4 +261,227 @@ public record AirTarget(InteractionHand hand, ItemStack stack) implements ToolTa Objects.requireNonNull(stack); } } + + /** Which part of using an item (eating, drawing a bow, ...) an {@link ItemUse} reports. */ + public enum UsePhase { + /** The entity starts using the item; the duration can be changed, or the use cancelled. */ + START, + /** Each tick of use; the use can be cancelled. */ + TICK, + /** The entity let go before the use finished, like releasing a bow. */ + STOP, + /** The use finished; the stack left in hand can be replaced. */ + FINISH + } + + /** + * Context of the {@code item-use-*} events: an entity using a bound item over time. Both sides. + * Results set here are written back to the NeoForge event after the handler runs. + */ + public static final class ItemUse { + private final UsePhase phase; + private final LivingEntity entity; + private final ItemStack stack; + private int duration; + private ItemStack result; + private boolean cancelled; + + /** + * @param duration ticks of use: the total at {@link UsePhase#START}, the ticks left otherwise + * @param result the stack left in hand at {@link UsePhase#FINISH}, else the used stack + */ + public ItemUse( + UsePhase phase, LivingEntity entity, ItemStack stack, int duration, ItemStack result) { + this.phase = Objects.requireNonNull(phase); + this.entity = Objects.requireNonNull(entity); + this.stack = Objects.requireNonNull(stack); + this.duration = duration; + this.result = Objects.requireNonNull(result); + } + + public UsePhase phase() { + return phase; + } + + public LivingEntity entity() { + return entity; + } + + public ItemStack stack() { + return stack; + } + + /** The total ticks of use at start; the ticks left in later phases. */ + public int duration() { + return duration; + } + + /** + * Changes the total ticks of use. + * + * @throws IllegalStateException outside {@link UsePhase#START} + */ + public void setDuration(int duration) { + if (phase != UsePhase.START) + throw new IllegalStateException("The use duration can only change when use starts"); + this.duration = Math.max(0, duration); + } + + /** The stack left in hand when use finishes. */ + public ItemStack result() { + return result; + } + + /** + * Replaces the stack left in hand. + * + * @throws IllegalStateException outside {@link UsePhase#FINISH} + */ + public void setResult(ItemStack result) { + if (phase != UsePhase.FINISH) + throw new IllegalStateException("The result can only be set when use finishes"); + this.result = Objects.requireNonNull(result); + } + + /** + * Stops the use. + * + * @throws IllegalStateException outside {@link UsePhase#START} and {@link UsePhase#TICK} + */ + public void cancel() { + if (phase != UsePhase.START && phase != UsePhase.TICK) + throw new IllegalStateException("Only starting or ongoing use can be cancelled"); + cancelled = true; + } + + public boolean cancelled() { + return cancelled; + } + } + + /** + * Context of {@code item-hit}: a melee hit with a bound item has dealt {@code damage} to {@code + * target}, after armor and other reductions. Server side. + */ + public record ItemHit(LivingEntity attacker, LivingEntity target, ItemStack stack, float damage) { + /** Requires every component. */ + public ItemHit { + Objects.requireNonNull(attacker); + Objects.requireNonNull(target); + Objects.requireNonNull(stack); + } + } + + /** + * Context of {@code item-break-block} (keyed by the held item) and {@code block-broken} (keyed by + * the block): a player is about to break a block. Server side, before the block is removed. + */ + public static final class BlockBreak { + // Blocks broken through mine() are not reported to item-break-block again, so a 3x3 tool does + // not set off 3x3 breaks of its own. + private static final ThreadLocal MINING = ThreadLocal.withInitial(() -> false); + + private final Player player; + private final ServerLevel level; + private final BlockPos position; + private final BlockState state; + private final ItemStack tool; + private final Direction face; + private boolean cancelled; + + /** @param face the face being mined, or null when it is not known */ + public BlockBreak( + Player player, + ServerLevel level, + BlockPos position, + BlockState state, + ItemStack tool, + Direction face) { + this.player = Objects.requireNonNull(player); + this.level = Objects.requireNonNull(level); + this.position = Objects.requireNonNull(position).immutable(); + this.state = Objects.requireNonNull(state); + this.tool = Objects.requireNonNull(tool); + this.face = face; + } + + public Player player() { + return player; + } + + public ServerLevel level() { + return level; + } + + public BlockPos position() { + return position; + } + + public BlockState state() { + return state; + } + + /** The item in the player's main hand; empty for bare hands. */ + public ItemStack tool() { + return tool; + } + + /** The face being mined, or null when it is not known. */ + public Direction face() { + return face; + } + + /** Keeps the block: it is not broken and drops nothing. */ + public void cancel() { + cancelled = true; + } + + public boolean cancelled() { + return cancelled; + } + + /** + * Breaks another block as this player would, with their tool: drops, experience, tool wear and + * other mods' break checks all apply. The block must be one the player could break; air, + * unbreakable blocks, blocks the tool cannot harvest, and this break's own block (which is + * already being broken) are skipped. + * + * @return whether the block was broken + */ + public boolean mine(BlockPos other) { + if (!(player instanceof net.minecraft.server.level.ServerPlayer server) + || other.equals(position)) return false; + var target = level.getBlockState(other); + if (target.isAir() + || target.getDestroySpeed(level, other) < 0 + || (target.requiresCorrectToolForDrops() && !tool.isCorrectToolForDrops(target))) + return false; + boolean outer = MINING.get(); + MINING.set(true); + try { + return server.gameMode.destroyBlock(other); + } finally { + MINING.set(outer); + } + } + + /** Whether a break now comes from {@link #mine}, inside another break's handler. */ + public static boolean mining() { + return MINING.get(); + } + } + + /** + * Context of {@code block-placed}, {@code block-stepped-on} and {@code block-random-tick}: something + * happened to a bound block. {@code entity} is the placer or the entity stepping on it, and null + * for random ticks. + */ + public record BlockAction(Level level, BlockPos position, BlockState state, Entity entity) { + /** Requires the level, position and state. */ + public BlockAction { + Objects.requireNonNull(level); + position = Objects.requireNonNull(position).immutable(); + Objects.requireNonNull(state); + } + } } diff --git a/src/main/java/zone/moddev/mc/lib/api/MaterialEventTypes.java b/src/main/java/zone/moddev/mc/lib/api/MaterialEventTypes.java index 012fa15f..5d2c538f 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MaterialEventTypes.java +++ b/src/main/java/zone/moddev/mc/lib/api/MaterialEventTypes.java @@ -1,6 +1,7 @@ package zone.moddev.mc.lib.api; import java.lang.invoke.MethodType; +import java.util.List; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; @@ -80,6 +81,80 @@ private MaterialEventTypes() {} public static final MaterialEvent REDSTONE_CHANGED = new MaterialEvent<>("redstone-changed", RedstoneChange.class); + /** + * A living entity starts using a bound item (eating, drawing a bow...), both sides; handler + * {@code (ItemUse)}, which can change the duration or cancel. + */ + public static final MaterialEvent ITEM_USE_START = + new MaterialEvent<>("item-use-start", ItemUse.class); + + /** Each tick of using a bound item, both sides; handler {@code (ItemUse)}, which can cancel. */ + public static final MaterialEvent ITEM_USE_TICK = + new MaterialEvent<>("item-use-tick", ItemUse.class); + + /** Use of a bound item stopped early, like a released bow, both sides; handler {@code (ItemUse)}. */ + public static final MaterialEvent ITEM_USE_STOP = + new MaterialEvent<>("item-use-stop", ItemUse.class); + + /** + * Use of a bound item finished, both sides; handler {@code (ItemUse)}, which can replace the stack + * left in hand. + */ + public static final MaterialEvent ITEM_USE_FINISH = + new MaterialEvent<>("item-use-finish", ItemUse.class); + + /** + * A melee hit with a bound item dealt damage, server side, after reductions; handler {@code + * (ItemHit)}. + */ + public static final MaterialEvent ITEM_HIT = new MaterialEvent<>("item-hit", ItemHit.class); + + /** + * A player holding a bound item is breaking a block, server side, before it is removed; handler + * {@code (BlockBreak)}, which can cancel or {@code mine} more blocks. + */ + public static final MaterialEvent ITEM_BREAK_BLOCK = + new MaterialEvent<>("item-break-block", BlockBreak.class); + + /** + * A player is breaking a bound block, server side, before it is removed; handler {@code + * (BlockBreak)}, which can cancel. + */ + public static final MaterialEvent BLOCK_BROKEN = + new MaterialEvent<>("block-broken", BlockBreak.class); + + /** An entity placed a bound block, server side; handler {@code (BlockAction)}. */ + public static final MaterialEvent BLOCK_PLACED = + new MaterialEvent<>("block-placed", BlockAction.class); + + /** + * An entity stepped on a bound block, both sides; fired by MMDLib's plain block ({@code + * mmdlib:block}) only; handler {@code (BlockAction)}. + */ + public static final MaterialEvent BLOCK_STEPPED_ON = + new MaterialEvent<>("block-stepped-on", BlockAction.class); + + /** + * A bound block got a random tick, server side; fired by MMDLib's plain block with random ticks + * on; handler {@code (BlockAction)}. + */ + public static final MaterialEvent BLOCK_RANDOM_TICK = + new MaterialEvent<>("block-random-tick", BlockAction.class); + + /** Every gameplay contract whose handler takes its context alone. */ + private static final List> SINGLE_CONTEXT = + List.of( + ITEM_USE_START, + ITEM_USE_TICK, + ITEM_USE_STOP, + ITEM_USE_FINISH, + ITEM_HIT, + ITEM_BREAK_BLOCK, + BLOCK_BROKEN, + BLOCK_PLACED, + BLOCK_STEPPED_ON, + BLOCK_RANDOM_TICK); + public static final MaterialEvent GUI_HOVER = new MaterialEvent<>("gui-hover", zone.moddev.mc.lib.gui.runtime.ScreenHover.class); @@ -87,6 +162,7 @@ private MaterialEventTypes() {} new MaterialEvent<>("gui-click", zone.moddev.mc.lib.gui.runtime.ScreenClick.class); static void install(MaterialEvents events) { + SINGLE_CONTEXT.forEach(event -> events.registerEvent(event.name(), event.contextType())); events.registerEvent(GUI_CLICK.name(), GUI_CLICK.contextType()); events.registerEvent(GUI_HOVER.name(), GUI_HOVER.contextType()); events.registerEvent( diff --git a/src/main/java/zone/moddev/mc/lib/common/runtime/BlockHooks.java b/src/main/java/zone/moddev/mc/lib/common/runtime/BlockHooks.java new file mode 100644 index 00000000..9b842264 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/common/runtime/BlockHooks.java @@ -0,0 +1,36 @@ +package zone.moddev.mc.lib.common.runtime; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +import zone.moddev.mc.lib.api.MaterialEvent; +import zone.moddev.mc.lib.api.MaterialEventContexts.BlockAction; +import zone.moddev.mc.lib.api.MaterialEventTypes; +import zone.moddev.mc.lib.api.MaterialEvents; +import zone.moddev.mc.lib.common.types.MMDBlock; +import zone.moddev.mc.lib.common.types.MMDMaterial; + +/** + * The block events only a block's own class can fire, as NeoForge has no event for them: {@code + * block-stepped-on} and {@code block-random-tick}. MMDLib's plain block and redstone block call + * these from their overrides. + */ +public record BlockHooks(MaterialEvents events, MMDBlock definition, MMDMaterial material) { + /** An entity stepped on the block. */ + public void steppedOn(Level level, BlockPos pos, BlockState state, Entity entity) { + fire(MaterialEventTypes.BLOCK_STEPPED_ON, new BlockAction(level, pos, state, entity)); + } + + /** The block got a random tick; only blocks with random ticks on get them. */ + public void randomTick(ServerLevel level, BlockPos pos, BlockState state) { + fire(MaterialEventTypes.BLOCK_RANDOM_TICK, new BlockAction(level, pos, state, null)); + } + + private void fire(MaterialEvent event, BlockAction context) { + var id = ItemEventBridge.binding(definition.events(), material, event.name()); + if (id != null) events.dispatch(id, event, context); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/common/runtime/ItemEventBridge.java b/src/main/java/zone/moddev/mc/lib/common/runtime/ItemEventBridge.java index 792eaa15..e52e0e44 100644 --- a/src/main/java/zone/moddev/mc/lib/common/runtime/ItemEventBridge.java +++ b/src/main/java/zone/moddev/mc/lib/common/runtime/ItemEventBridge.java @@ -17,7 +17,13 @@ import net.minecraft.world.item.ItemStack; import net.neoforged.bus.api.IEventBus; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; import net.neoforged.neoforge.event.entity.living.LivingDamageEvent; +import net.neoforged.neoforge.event.entity.living.LivingEntityUseItemEvent; +import net.neoforged.neoforge.event.level.BlockEvent; import net.neoforged.neoforge.event.entity.living.LivingIncomingDamageEvent; import net.neoforged.neoforge.event.entity.player.AttackEntityEvent; import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent; @@ -82,6 +88,118 @@ public void register(IEventBus bus) { bus.addListener(this::playerTick); bus.addListener(this::projectileDamage); bus.addListener(this::projectileHit); + bus.addListener(this::useStart); + bus.addListener(this::useTick); + bus.addListener(this::useStop); + bus.addListener(this::useFinish); + bus.addListener(this::meleeHit); + bus.addListener(this::blockBreak); + bus.addListener(this::blockPlace); + } + + private void useStart(LivingEntityUseItemEvent.Start event) { + var use = use(event, UsePhase.START, MaterialEventTypes.ITEM_USE_START); + if (use == null) return; + event.setDuration(use.duration()); + if (use.cancelled()) event.setCanceled(true); + } + + private void useTick(LivingEntityUseItemEvent.Tick event) { + var use = use(event, UsePhase.TICK, MaterialEventTypes.ITEM_USE_TICK); + if (use != null && use.cancelled()) event.setCanceled(true); + } + + private void useStop(LivingEntityUseItemEvent.Stop event) { + use(event, UsePhase.STOP, MaterialEventTypes.ITEM_USE_STOP); + } + + private void useFinish(LivingEntityUseItemEvent.Finish event) { + var id = resolve(event.getItem(), MaterialEventTypes.ITEM_USE_FINISH); + if (id == null) return; + var use = + new ItemUse( + UsePhase.FINISH, + event.getEntity(), + event.getItem(), + event.getDuration(), + event.getResultStack()); + events.dispatch(id, MaterialEventTypes.ITEM_USE_FINISH, use); + event.setResultStack(use.result()); + } + + /** Dispatches a use phase; null when the item binds no handler for it. */ + private ItemUse use(LivingEntityUseItemEvent event, UsePhase phase, MaterialEvent type) { + var id = resolve(event.getItem(), type); + if (id == null) return null; + var use = new ItemUse(phase, event.getEntity(), event.getItem(), event.getDuration(), event.getItem()); + events.dispatch(id, type, use); + return use; + } + + /** + * Server only, after damage was dealt. A melee hit is damage whose direct source is the attacker + * itself, which excludes projectiles, thorns and the like; the attacker's main-hand item is bound. + */ + private void meleeHit(LivingDamageEvent.Post event) { + var target = event.getEntity(); + var source = event.getSource(); + if (target.level().isClientSide + || !(source.getEntity() instanceof LivingEntity attacker) + || source.getDirectEntity() != attacker) return; + var stack = attacker.getMainHandItem(); + dispatch( + stack, MaterialEventTypes.ITEM_HIT, new ItemHit(attacker, target, stack, event.getNewDamage())); + } + + /** + * Server only, before the block is removed: the held item's {@code item-break-block} first + * (skipped for blocks broken by its own {@link BlockBreak#mine}), then the block's {@code + * block-broken}. Either can cancel the break. + */ + private void blockBreak(BlockEvent.BreakEvent event) { + if (!(event.getLevel() instanceof ServerLevel level)) return; + var player = event.getPlayer(); + var tool = player.getMainHandItem(); + var itemHandler = BlockBreak.mining() ? null : resolve(tool, MaterialEventTypes.ITEM_BREAK_BLOCK); + var block = BuiltInRegistries.BLOCK.getKey(event.getState().getBlock()); + var definition = blockDefinitions.apply(block); + var blockHandler = + definition == null + ? null + : binding( + definition.events(), blockMaterials.apply(block), MaterialEventTypes.BLOCK_BROKEN.name()); + if (itemHandler == null && blockHandler == null) return; + var context = + new BlockBreak(player, level, event.getPos(), event.getState(), tool, face(player, event)); + if (itemHandler != null) + events.dispatch(itemHandler, MaterialEventTypes.ITEM_BREAK_BLOCK, context); + if (blockHandler != null && !context.cancelled()) + events.dispatch(blockHandler, MaterialEventTypes.BLOCK_BROKEN, context); + if (context.cancelled()) event.setCanceled(true); + } + + /** The face the player is looking at on the broken block, or null when it is out of sight. */ + private static Direction face(Player player, BlockEvent.BreakEvent event) { + var hit = player.pick(player.blockInteractionRange() + 1, 0, false); + return hit instanceof BlockHitResult block && block.getBlockPos().equals(event.getPos()) + ? block.getDirection() + : null; + } + + /** Server only, after the block is placed. */ + private void blockPlace(BlockEvent.EntityPlaceEvent event) { + if (!(event.getLevel() instanceof Level level)) return; + var placed = event.getPlacedBlock(); + var block = BuiltInRegistries.BLOCK.getKey(placed.getBlock()); + var definition = blockDefinitions.apply(block); + if (definition == null) return; + var id = + binding(definition.events(), blockMaterials.apply(block), MaterialEventTypes.BLOCK_PLACED.name()); + if (id != null) + events.dispatch( + id, + MaterialEventTypes.BLOCK_PLACED, + new BlockAction(level, event.getPos(), placed, event.getEntity())); } /** diff --git a/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java b/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java index a05ade4b..d5fa42f4 100644 --- a/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java +++ b/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java @@ -15,6 +15,12 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.properties.DirectionProperty; import net.minecraft.world.level.block.state.properties.IntegerProperty; import zone.moddev.mc.lib.api.MaterialEventContexts.RedstoneChange; @@ -42,11 +48,24 @@ public class RedstoneBlock extends Block { */ public static final IntegerProperty OUTPUT = IntegerProperty.create("output", 0, 15); + /** The way the block faces: its front. Present when {@code facing} is set. */ + // Created here rather than taken from BlockStateProperties, whose class initialization needs a + // bootstrapped game; the name and values are the same. + public static final DirectionProperty FACING = + DirectionProperty.create("facing", Direction.Plane.HORIZONTAL); + /** * {@code input}: track received power and fire {@code redstone-changed}. {@code output}: a * constant 0-15, or {@code "state"} for power stored in the block. {@code strong}: also power * through the touching block. {@code faces}: faces that emit (all by default). {@code - * comparator}: a constant 0-15, {@code "input"}, or {@code "output"}. + * comparator}: a constant 0-15, {@code "input"}, or {@code "output"}. {@code facing}: the block + * faces the way its placer looked, and {@code faces} and {@code inputs} may name {@code front}, + * {@code back}, {@code left} and {@code right}. {@code inputs}: the faces read (all by default). + * {@code delay}: ticks between a neighbor change and reading the inputs (0, the default, reads at + * once). + * + *

With {@code facing}, {@link #faces} and {@link #inputs} hold the faces of a block facing + * north: front is north, back south, left west, right east. */ public record Settings( boolean input, @@ -55,7 +74,40 @@ public record Settings( boolean strong, Set faces, Optional constantComparator, - Optional comparatorProperty) { + Optional comparatorProperty, + boolean facing, + Set inputs, + int delay) { + /** Settings without facing, reading every face at once. */ + public Settings( + boolean input, + Optional constantOutput, + boolean stateOutput, + boolean strong, + Set faces, + Optional constantComparator, + Optional comparatorProperty) { + this( + input, + constantOutput, + stateOutput, + strong, + faces, + constantComparator, + comparatorProperty, + false, + Set.of(Direction.values()), + 0); + } + + /** + * Whether {@code redstone-changed} fires on any read face's change rather than only when the + * strongest input changes: true when only some faces are read. + */ + public boolean perFace() { + return inputs.size() < Direction.values().length; + } + /** Whether the block outputs any redstone signal. */ public boolean emits() { return stateOutput || constantOutput.filter(power -> power > 0).isPresent(); @@ -73,7 +125,8 @@ public static Optional of(MaterialPart part) { throw error(part, "redstone must be an object"); JsonObject redstone = json.getAsJsonObject().getAsJsonObject("redstone"); for (String key : redstone.keySet()) - if (!Set.of("input", "output", "strong", "faces", "comparator").contains(key)) + if (!Set.of("input", "output", "strong", "faces", "comparator", "facing", "inputs", "delay") + .contains(key)) throw error(part, "unknown redstone key '" + key + "'"); boolean input = redstone.has("input") && redstone.get("input").getAsBoolean(); Optional constant = Optional.empty(); @@ -87,15 +140,15 @@ public static Optional of(MaterialPart part) { } else constant = Optional.of(power(part, output, "output")); } boolean strong = redstone.has("strong") && redstone.get("strong").getAsBoolean(); - Set faces = EnumSet.allOf(Direction.class); - if (redstone.has("faces")) { - faces = EnumSet.noneOf(Direction.class); - for (JsonElement face : redstone.getAsJsonArray("faces")) { - Direction direction = Direction.byName(face.getAsString()); - if (direction == null) throw error(part, "unknown face '" + face.getAsString() + "'"); - faces.add(direction); - } - } + boolean facing = redstone.has("facing") && redstone.get("facing").getAsBoolean(); + Set faces = faceList(part, redstone, "faces", facing); + Set inputs = faceList(part, redstone, "inputs", facing); + if (redstone.has("inputs") && !input) throw error(part, "inputs needs input: true"); + if (redstone.has("inputs") && inputs.isEmpty()) + throw error(part, "inputs must name at least one face"); + int delay = redstone.has("delay") ? redstone.get("delay").getAsInt() : 0; + if (delay < 0 || delay > 100) throw error(part, "delay must be 0-100 ticks"); + if (delay > 0 && !input) throw error(part, "delay needs input: true"); Optional comparator = Optional.empty(); Optional comparatorProperty = Optional.empty(); if (redstone.has("comparator")) { @@ -122,7 +175,48 @@ public static Optional of(MaterialPart part) { throw error(part, "faces must name at least one face"); return Optional.of( new Settings( - input, constant, state, strong, Set.copyOf(faces), comparator, comparatorProperty)); + input, + constant, + state, + strong, + Set.copyOf(faces), + comparator, + comparatorProperty, + facing, + Set.copyOf(inputs), + delay)); + } + + /** A face list, all faces when absent; relative names need {@code facing}. */ + private static Set faceList( + MaterialPart part, JsonObject redstone, String key, boolean facing) { + if (!redstone.has(key)) return EnumSet.allOf(Direction.class); + if (!redstone.get(key).isJsonArray()) throw error(part, key + " must be a list of faces"); + Set faces = EnumSet.noneOf(Direction.class); + for (JsonElement face : redstone.getAsJsonArray(key)) { + String name = face.getAsString(); + Direction direction = + switch (name) { + case "front", "back", "left", "right" -> { + if (!facing) throw error(part, "'" + name + "' in " + key + " needs facing: true"); + yield relative(name); + } + default -> Direction.byName(name); + }; + if (direction == null) throw error(part, "unknown face '" + name + "'"); + faces.add(direction); + } + return faces; + } + + /** A relative face name as the face of a block facing north. */ + private static Direction relative(String name) { + return switch (name) { + case "front" -> Direction.NORTH; + case "back" -> Direction.SOUTH; + case "left" -> Direction.WEST; + default -> Direction.EAST; + }; } private static int power(MaterialPart part, JsonElement value, String key) { @@ -142,6 +236,7 @@ private static IllegalArgumentException error(MaterialPart part, String message) private final MaterialEvents events; private final MMDBlock definition; private final MMDMaterial material; + private final BlockHooks hooks; /** * A block with the given redstone settings; {@code redstone-changed} is dispatched through {@code @@ -159,6 +254,19 @@ public RedstoneBlock( this.events = events; this.definition = definition; this.material = material; + hooks = new BlockHooks(events, definition, material); + } + + @Override + public void stepOn(Level level, BlockPos pos, BlockState state, Entity entity) { + super.stepOn(level, pos, state, entity); + hooks.steppedOn(level, pos, state, entity); + } + + @Override + protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { + super.randomTick(state, level, pos, random); + hooks.randomTick(level, pos, state); } private static Properties constructing(Properties properties, Settings settings) { @@ -171,6 +279,57 @@ protected void createBlockStateDefinition(StateDefinition.Builder new RedstoneBlock( c.properties(), settings, c.events(), c.definition(), c.material())) - .orElseGet(() -> new Block(c.properties())), + .orElseGet( + () -> + new StandardBlocks.Plain( + c.properties(), + new BlockHooks(c.events(), c.definition(), c.material()))), RedstoneBlock.Settings::of, null)); block( diff --git a/src/main/java/zone/moddev/mc/lib/common/runtime/StandardItems.java b/src/main/java/zone/moddev/mc/lib/common/runtime/StandardItems.java index f7c5630b..ef6845e5 100644 --- a/src/main/java/zone/moddev/mc/lib/common/runtime/StandardItems.java +++ b/src/main/java/zone/moddev/mc/lib/common/runtime/StandardItems.java @@ -300,15 +300,32 @@ Item.Properties apply(MaterialToolTier tier, Item.Properties properties) { public static class Basic extends Item implements MaterialBacked { private final MMDMaterial material; private final java.util.function.Supplier repair; + private final int useDuration; + private final net.minecraft.world.item.UseAnim useAnimation; /** A plain item; {@code repair} gives the ingredient that repairs it. */ public Basic( MMDMaterial material, java.util.function.Supplier repair, Item.Properties properties) { + this(material, repair, 0, net.minecraft.world.item.UseAnim.NONE, properties); + } + + /** + * A plain item that is used over {@code useDuration} ticks with {@code useAnimation} when + * right-clicked, firing the {@code item-use-*} events; 0 for an item that is not used. + */ + public Basic( + MMDMaterial material, + java.util.function.Supplier repair, + int useDuration, + net.minecraft.world.item.UseAnim useAnimation, + Item.Properties properties) { super(properties); this.material = Objects.requireNonNull(material); this.repair = Objects.requireNonNull(repair); + this.useDuration = useDuration; + this.useAnimation = Objects.requireNonNull(useAnimation); } @Override @@ -320,6 +337,27 @@ public MMDMaterial material() { public boolean isValidRepairItem(ItemStack damaged, ItemStack ingredient) { return repair.get().test(ingredient) || super.isValidRepairItem(damaged, ingredient); } + + // A use time of 0 keeps vanilla behavior, including food components. + @Override + public int getUseDuration(ItemStack stack, net.minecraft.world.entity.LivingEntity entity) { + return useDuration > 0 ? useDuration : super.getUseDuration(stack, entity); + } + + @Override + public net.minecraft.world.item.UseAnim getUseAnimation(ItemStack stack) { + return useDuration > 0 ? useAnimation : super.getUseAnimation(stack); + } + + @Override + public net.minecraft.world.InteractionResultHolder use( + net.minecraft.world.level.Level level, + net.minecraft.world.entity.player.Player player, + net.minecraft.world.InteractionHand hand) { + if (useDuration <= 0) return super.use(level, player, hand); + player.startUsingItem(hand); + return net.minecraft.world.InteractionResultHolder.consume(player.getItemInHand(hand)); + } } /** {@code mmdlib:bow}: a vanilla bow made of the material, repairable with it. */ @@ -502,7 +540,22 @@ public static class Fuel extends Basic { /** A plain item that burns for {@code burn} ticks. */ public Fuel(MMDMaterial material, int burn, Item.Properties properties) { - super(material, () -> net.minecraft.world.item.crafting.Ingredient.EMPTY, properties); + this(material, burn, 0, net.minecraft.world.item.UseAnim.NONE, properties); + } + + /** A plain item that burns for {@code burn} ticks and is used as {@link Basic} describes. */ + public Fuel( + MMDMaterial material, + int burn, + int useDuration, + net.minecraft.world.item.UseAnim useAnimation, + Item.Properties properties) { + super( + material, + () -> net.minecraft.world.item.crafting.Ingredient.EMPTY, + useDuration, + useAnimation, + properties); this.burn = burn; } diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockBuilder.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockBuilder.java index 96107f18..59a7480e 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockBuilder.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockBuilder.java @@ -114,9 +114,20 @@ public void setBlockEntityData(IData data) { * @throws IllegalArgumentException if a value is invalid * @throws IllegalStateException if the name was already built */ + @Override + ResourceLocation definitionId() { + return id; + } + + @Override + ResourceLocation factoryId() { + return ScriptDefinitions.catalog().blockModule(module).factory(); + } + @ZenCodeType.Method @Override public void build() { + validateBlock(); var d = MaterialRuntimeSettings.DEFAULT; var runtime = new MaterialRuntimeSettings( diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockContext.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockContext.java new file mode 100644 index 00000000..f6bd9101 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockContext.java @@ -0,0 +1,109 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.Locale; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.common.runtime.RedstoneBlock; + +/** + * The block a script handler runs for: where it is, the power reaching each side, and the power it + * emits. Sides are {@code up}, {@code down}, {@code north}, {@code south}, {@code east} and {@code + * west}, and for blocks that face a way also {@code front}, {@code back}, {@code left} and {@code + * right}. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.BlockContext") +public abstract class ScriptBlockContext { + private final Level level; + private final BlockPos pos; + + ScriptBlockContext(Level level, BlockPos pos) { + this.level = level; + this.pos = pos.immutable(); + } + + /** Whether this runs on the client; outputs can only change on the server. */ + @ZenCodeType.Getter("client") + public boolean isClient() { + return level.isClientSide(); + } + + @ZenCodeType.Getter("x") + public int getX() { + return pos.getX(); + } + + @ZenCodeType.Getter("y") + public int getY() { + return pos.getY(); + } + + @ZenCodeType.Getter("z") + public int getZ() { + return pos.getZ(); + } + + /** The way the block faces, such as {@code north}; empty for blocks that do not face a way. */ + @ZenCodeType.Getter("facing") + public String getFacing() { + var state = level.getBlockState(pos); + return state.hasProperty(RedstoneBlock.FACING) + ? state.getValue(RedstoneBlock.FACING).getSerializedName() + : ""; + } + + /** The power, 0 to 15, reaching the block's {@code side} now. */ + @ZenCodeType.Method + public int input(String side) { + return RedstoneBlock.signal(level, pos, direction(side)); + } + + /** The power the block emits, 0 to 15. */ + @ZenCodeType.Getter("output") + public int getOutput() { + var state = level.getBlockState(pos); + if (state.hasProperty(RedstoneBlock.OUTPUT)) return state.getValue(RedstoneBlock.OUTPUT); + return state.getBlock() instanceof RedstoneBlock block + ? block.settings().constantOutput().orElse(0) + : 0; + } + + /** + * Sets the power the block emits, clamped to 0 to 15. Ignored on the client; the block needs + * {@code redstoneOutput = true}. + */ + @ZenCodeType.Setter("output") + public void setOutput(int power) { + if (!level.isClientSide()) RedstoneBlock.setOutput(level, pos, power); + } + + /** {@code side} as a world direction. */ + Direction direction(String side) { + var name = side.toLowerCase(Locale.ROOT); + var state = level.getBlockState(pos); + Direction local = + switch (name) { + case "front" -> Direction.NORTH; + case "back" -> Direction.SOUTH; + case "left" -> Direction.WEST; + case "right" -> Direction.EAST; + default -> null; + }; + if (local == null) { + var world = Direction.byName(name); + if (world == null) throw new IllegalArgumentException("Unknown side: " + side); + return world; + } + if (!state.hasProperty(RedstoneBlock.FACING)) + throw new IllegalArgumentException( + "Side '" + side + "' needs a block that faces a way (directional = true)"); + return RedstoneBlock.rotate(local, state.getValue(RedstoneBlock.FACING)); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockEventContext.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockEventContext.java new file mode 100644 index 00000000..57e90849 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockEventContext.java @@ -0,0 +1,31 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import net.minecraft.world.entity.Entity; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.api.MaterialEventContexts.BlockAction; + +/** + * What {@code onPlaced}, {@code onSteppedOn} and {@code onRandomTick} handlers receive: the block, + * and the entity that placed it or stepped on it. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.BlockEventContext") +public final class ScriptBlockEventContext extends ScriptBlockContext { + private final BlockAction action; + + ScriptBlockEventContext(BlockAction action) { + super(action.level(), action.position()); + this.action = action; + } + + /** The entity that placed the block or stepped on it; null for random ticks. */ + @ZenCodeType.Nullable + @ZenCodeType.Getter("entity") + public Entity getEntity() { + return action.entity(); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockEventHandler.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockEventHandler.java new file mode 100644 index 00000000..15b31808 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockEventHandler.java @@ -0,0 +1,14 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import org.openzen.zencode.java.ZenCodeType; + +/** A script function run when a block is placed, stepped on or randomly ticked. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.BlockEventHandler") +@FunctionalInterface +public interface ScriptBlockEventHandler { + @ZenCodeType.Method + void handle(ScriptBlockEventContext context); +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModule.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModule.java index 55e5b536..304a7f03 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModule.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModule.java @@ -102,9 +102,20 @@ public void setMaterialDefaults(boolean materialDefaults) { * * @throws IllegalArgumentException if a value is invalid or the name is taken */ + @Override + ResourceLocation definitionId() { + return id; + } + + @Override + ResourceLocation factoryId() { + return factory; + } + @ZenCodeType.Method @Override public void build() { + validateBlock(); var part = base.part(); ScriptDefinitions.catalog() .registerBlockModule( diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java index 965a0928..bba61010 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java @@ -1,23 +1,35 @@ package zone.moddev.mc.lib.compat.CraftTweaker; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Optional; +import java.util.Set; import com.blamejared.crafttweaker.api.annotation.ZenRegister; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import net.minecraft.resources.ResourceLocation; + import org.openzen.zencode.java.ZenCodeType; +import zone.moddev.mc.lib.api.MaterialEventTypes; import zone.moddev.mc.lib.common.codecs.BlockTemplate; +import zone.moddev.mc.lib.common.runtime.RedstoneBlock; import zone.moddev.mc.lib.common.types.FactoryData; import zone.moddev.mc.lib.common.types.MMDBlock; +import zone.moddev.mc.lib.common.types.MaterialPart; /** - * What a script block and a script block module share: the block's properties and whether it has - * an item, besides the tags, events and factory data of every definition. Values start from the - * module the builder is based on. + * What a script block and a script block module share: the block's properties, whether it has an + * item, redstone behavior and script handlers, besides the tags, events and factory data of every + * definition. Values start from the module the builder is based on. */ @ZenRegister(loaders = ScriptDefinitions.LOADER) @ZenCodeType.Name("mods.mmdlib.BlockProperties") public abstract class ScriptBlockProperties extends ScriptDefinitionProperties { + private static final ResourceLocation BLOCK_FACTORY = ResourceLocation.parse("mmdlib:block"); private final BlockTemplate base; private float hardness; private float blastResistance; @@ -33,10 +45,33 @@ public abstract class ScriptBlockProperties extends ScriptDefinitionProperties { private String sound; private String mapColor; private boolean hasItem; + // Redstone settings, written to factory_data.redstone when any is set. + private boolean redstone; + private boolean redstoneInput; + private boolean redstoneOutput; + private boolean strongPower; + private boolean directional; + private int redstoneDelay = 1; + private String comparator = ""; + private Set outputFaces; + private Set inputFaces; + private boolean classHooks; ScriptBlockProperties(BlockTemplate base, FactoryData factoryData) { super(base.tags(), base.events(), factoryData); this.base = base; + var json = factoryData.json(); + if (json.isJsonObject() && json.getAsJsonObject().get("redstone") instanceof JsonObject r) { + redstone = true; + redstoneInput = r.has("input") && r.get("input").getAsBoolean(); + redstoneOutput = r.has("output") && r.get("output").getAsString().equals("state"); + strongPower = r.has("strong") && r.get("strong").getAsBoolean(); + directional = r.has("facing") && r.get("facing").getAsBoolean(); + redstoneDelay = r.has("delay") ? r.get("delay").getAsInt() : 0; + comparator = r.has("comparator") ? r.get("comparator").getAsString() : ""; + outputFaces = faces(r, "faces"); + inputFaces = faces(r, "inputs"); + } var p = base.properties(); hardness = p.hardness(); blastResistance = p.blastResistance(); @@ -208,6 +243,295 @@ public void setHasItem(boolean hasItem) { this.hasItem = hasItem; } + /** The Java implementation that builds the block. */ + abstract ResourceLocation factoryId(); + + /** + * Whether the block reads the redstone power reaching it and runs {@code onRedstoneChanged}. + * Needs the {@code mmdlib:block} module or factory, as do the other redstone settings. + */ + @ZenCodeType.Getter("redstoneInput") + public boolean getRedstoneInput() { + return redstoneInput; + } + + @ZenCodeType.Setter("redstoneInput") + public void setRedstoneInput(boolean redstoneInput) { + redstone = true; + this.redstoneInput = redstoneInput; + } + + /** Whether the block emits a power that handlers set with {@code context.output}. */ + @ZenCodeType.Getter("redstoneOutput") + public boolean getRedstoneOutput() { + return redstoneOutput; + } + + @ZenCodeType.Setter("redstoneOutput") + public void setRedstoneOutput(boolean redstoneOutput) { + redstone = true; + this.redstoneOutput = redstoneOutput; + } + + /** Whether the output also powers through the touching block, like a lever's. */ + @ZenCodeType.Getter("strongPower") + public boolean getStrongPower() { + return strongPower; + } + + @ZenCodeType.Setter("strongPower") + public void setStrongPower(boolean strongPower) { + redstone = true; + this.strongPower = strongPower; + } + + /** + * Whether the block faces the way its placer looked, so faces can be named {@code front}, {@code + * back}, {@code left} and {@code right}. + */ + @ZenCodeType.Getter("directional") + public boolean getDirectional() { + return directional; + } + + @ZenCodeType.Setter("directional") + public void setDirectional(boolean directional) { + redstone = true; + this.directional = directional; + } + + /** + * Ticks between a change in power and {@code onRedstoneChanged}; 1 by default, like a fast + * repeater. 0 reacts at once, but blocks wired into a loop then recurse. + */ + @ZenCodeType.Getter("redstoneDelay") + public int getRedstoneDelay() { + return redstoneDelay; + } + + @ZenCodeType.Setter("redstoneDelay") + public void setRedstoneDelay(int redstoneDelay) { + redstone = true; + this.redstoneDelay = redstoneDelay; + } + + /** What a comparator reads: {@code "input"}, {@code "output"}, or {@code ""} for nothing. */ + @ZenCodeType.Getter("comparator") + public String getComparator() { + return comparator; + } + + @ZenCodeType.Setter("comparator") + public void setComparator(String comparator) { + redstone = true; + this.comparator = comparator; + } + + /** The sides the output leaves from; every side unless set. */ + @ZenCodeType.Getter("outputFaces") + public String[] getOutputFaces() { + return outputFaces == null ? new String[] {ALL} : outputFaces.toArray(String[]::new); + } + + /** Adds sides the output leaves from; the first call replaces "every side". */ + @ZenCodeType.Method + public ScriptBlockProperties addOutputFaces(String... faces) { + outputFaces = add(outputFaces, faces); + return this; + } + + /** Removes output sides; {@code "all"} removes every side, leaving the block silent. */ + @ZenCodeType.Method + public ScriptBlockProperties removeOutputFaces(String... faces) { + outputFaces = remove(outputFaces, faces); + return this; + } + + /** The sides the block reads; every side unless set. */ + @ZenCodeType.Getter("inputFaces") + public String[] getInputFaces() { + return inputFaces == null ? new String[] {ALL} : inputFaces.toArray(String[]::new); + } + + /** + * Adds sides the block reads; the first call replaces "every side". Reading only some sides runs + * {@code onRedstoneChanged} when any of them changes. + */ + @ZenCodeType.Method + public ScriptBlockProperties addInputFaces(String... faces) { + inputFaces = add(inputFaces, faces); + return this; + } + + /** Removes read sides; {@code "all"} removes every side. */ + @ZenCodeType.Method + public ScriptBlockProperties removeInputFaces(String... faces) { + inputFaces = remove(inputFaces, faces); + return this; + } + + /** + * Runs {@code handler} on the server when the power reaching the block changes, after {@code + * redstoneDelay} ticks. Turns on {@code redstoneInput}. + */ + @ZenCodeType.Method + public ScriptBlockProperties onRedstoneChanged(ScriptRedstoneHandler handler) { + hook( + MaterialEventTypes.REDSTONE_CHANGED, + handler, + (h, change) -> h.handle(new ScriptRedstoneContext(change))); + setRedstoneInput(true); + return this; + } + + /** Runs {@code handler} when a player right-clicks the block, on both sides. */ + @ZenCodeType.Method + public ScriptBlockProperties onRightClick(ScriptClickHandler handler) { + hook( + MaterialEventTypes.BLOCK_RIGHT_CLICK, + handler, + (h, click) -> { + click.use().consume(); + h.handle(new ScriptClickContext(click)); + }); + return this; + } + + /** + * Runs {@code handler} on the server when a player is about to break the block; it can cancel the + * break. + */ + @ZenCodeType.Method + public ScriptBlockProperties onBroken(ScriptBreakHandler handler) { + hook( + MaterialEventTypes.BLOCK_BROKEN, + handler, + (h, broken) -> h.handle(new ScriptBreakContext(broken))); + return this; + } + + /** Runs {@code handler} on the server when an entity places the block. */ + @ZenCodeType.Method + public ScriptBlockProperties onPlaced(ScriptBlockEventHandler handler) { + hook( + MaterialEventTypes.BLOCK_PLACED, + handler, + (h, action) -> h.handle(new ScriptBlockEventContext(action))); + return this; + } + + /** + * Runs {@code handler} when an entity steps on the block, on both sides. Needs the plain block + * ({@code mmdlib:block}), whose class fires it. + */ + @ZenCodeType.Method + public ScriptBlockProperties onSteppedOn(ScriptBlockEventHandler handler) { + classHooks = true; + hook( + MaterialEventTypes.BLOCK_STEPPED_ON, + handler, + (h, action) -> h.handle(new ScriptBlockEventContext(action))); + return this; + } + + /** + * Runs {@code handler} on the server when the block gets a random tick, like crops growing. Turns + * on {@code randomTicks}. Needs the plain block ({@code mmdlib:block}). + */ + @ZenCodeType.Method + public ScriptBlockProperties onRandomTick(ScriptBlockEventHandler handler) { + classHooks = true; + setRandomTicks(true); + hook( + MaterialEventTypes.BLOCK_RANDOM_TICK, + handler, + (h, action) -> h.handle(new ScriptBlockEventContext(action))); + return this; + } + + /** + * Checks the redstone settings and class-only hooks now, so the error points at the script. + * + * @throws IllegalArgumentException if they are invalid or the factory is not {@code + * mmdlib:block} + */ + void validateBlock() { + if (classHooks && !factoryId().equals(BLOCK_FACTORY)) + throw new IllegalArgumentException( + "onSteppedOn and onRandomTick need a block built by " + + BLOCK_FACTORY + + ", whose class fires them, not " + + factoryId() + + "; NeoForge has no event for them"); + if (!redstone) return; + if (!factoryId().equals(BLOCK_FACTORY)) + throw new IllegalArgumentException( + "Redstone settings need a block built by " + BLOCK_FACTORY + ", not " + factoryId()); + RedstoneBlock.Settings.of( + new MaterialPart( + definitionId(), + MaterialPart.Kind.BLOCK, + Optional.empty(), + zone.moddev.mc.lib.common.types.PartMetadata.EMPTY, + factoryId(), + factoryData())); + } + + @Override + FactoryData factoryData() { + var data = super.factoryData(); + if (!redstone) return data; + var json = data.json().getAsJsonObject(); + var settings = new JsonObject(); + if (redstoneInput) settings.addProperty("input", true); + if (redstoneOutput) settings.addProperty("output", "state"); + if (strongPower) settings.addProperty("strong", true); + if (directional) settings.addProperty("facing", true); + if (redstoneInput && redstoneDelay > 0) settings.addProperty("delay", redstoneDelay); + if (!comparator.isEmpty()) settings.addProperty("comparator", comparator); + if (outputFaces != null) settings.add("faces", array(outputFaces)); + if (inputFaces != null && redstoneInput) settings.add("inputs", array(inputFaces)); + json.add("redstone", settings); + return new FactoryData(json); + } + + private static Set add(Set current, String[] faces) { + var result = current == null ? new LinkedHashSet() : new LinkedHashSet<>(current); + for (var face : faces) { + if (face.equals(ALL)) throw new IllegalArgumentException("\"all\" can only remove faces"); + result.add(face); + } + return result; + } + + // "Every side" in the naming the block uses: relative names for a directional block. + private Set remove(Set current, String[] faces) { + var result = + current == null + ? new LinkedHashSet<>( + directional + ? List.of("up", "down", "front", "back", "left", "right") + : List.of("up", "down", "north", "south", "east", "west")) + : new LinkedHashSet<>(current); + for (var face : faces) + if (face.equals(ALL)) result.clear(); + else result.remove(face); + return result; + } + + private static Set faces(JsonObject redstone, String key) { + if (!redstone.has(key)) return null; + var faces = new LinkedHashSet(); + redstone.getAsJsonArray(key).forEach(face -> faces.add(face.getAsString())); + return faces; + } + + private static JsonArray array(Set faces) { + var array = new JsonArray(); + faces.forEach(array::add); + return array; + } + /** The template these values describe. */ BlockTemplate template() { return new BlockTemplate( diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBreakContext.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBreakContext.java new file mode 100644 index 00000000..b6b7d1c4 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBreakContext.java @@ -0,0 +1,82 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; +import com.blamejared.crafttweaker.api.item.IItemStack; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.entity.player.Player; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.api.MaterialEventContexts.BlockBreak; + +/** + * What {@code onBreakBlock} (items) and {@code onBroken} (blocks) handlers receive: a player about + * to break a block. Server side. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.BreakContext") +public final class ScriptBreakContext { + private final BlockBreak broken; + + ScriptBreakContext(BlockBreak broken) { + this.broken = broken; + } + + @ZenCodeType.Getter("player") + public Player getPlayer() { + return broken.player(); + } + + /** The item in the player's main hand. */ + @ZenCodeType.Getter("tool") + public IItemStack getTool() { + return IItemStack.of(broken.tool()); + } + + /** The block's ID, such as {@code minecraft:stone}. */ + @ZenCodeType.Getter("block") + public String getBlock() { + return BuiltInRegistries.BLOCK.getKey(broken.state().getBlock()).toString(); + } + + @ZenCodeType.Getter("x") + public int getX() { + return broken.position().getX(); + } + + @ZenCodeType.Getter("y") + public int getY() { + return broken.position().getY(); + } + + @ZenCodeType.Getter("z") + public int getZ() { + return broken.position().getZ(); + } + + /** The face being mined, such as {@code up}; empty when it is not known. */ + @ZenCodeType.Getter("face") + public String getFace() { + return broken.face() == null ? "" : broken.face().getSerializedName(); + } + + /** Keeps the block: it is not broken and drops nothing. */ + @ZenCodeType.Method + public void cancel() { + broken.cancel(); + } + + /** + * Breaks the block at {@code x, y, z} as this player would with their tool: drops, experience and + * tool wear apply. Air, unbreakable blocks and blocks the tool cannot harvest are skipped, and + * the extra breaks do not run the item's handler again. + * + * @return whether the block was broken + */ + @ZenCodeType.Method + public boolean mine(int x, int y, int z) { + return broken.mine(new BlockPos(x, y, z)); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBreakHandler.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBreakHandler.java new file mode 100644 index 00000000..80b9e8d4 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBreakHandler.java @@ -0,0 +1,14 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import org.openzen.zencode.java.ZenCodeType; + +/** A script function run when a block is about to be broken. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.BreakHandler") +@FunctionalInterface +public interface ScriptBreakHandler { + @ZenCodeType.Method + void handle(ScriptBreakContext context); +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptClickContext.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptClickContext.java new file mode 100644 index 00000000..1018f53d --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptClickContext.java @@ -0,0 +1,42 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import net.minecraft.world.entity.player.Player; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.api.MaterialEventContexts.BlockClick; + +/** + * What an {@code onRightClick} handler receives: the block, who clicked it and on which face. The + * handler runs on both sides; the click is used up, so the held item does nothing. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ClickContext") +public final class ScriptClickContext extends ScriptBlockContext { + private final BlockClick click; + + ScriptClickContext(BlockClick click) { + super(click.use().level(), click.use().position()); + this.click = click; + } + + /** The player who clicked. */ + @ZenCodeType.Getter("player") + public Player getPlayer() { + return click.player(); + } + + /** Whether the player was sneaking. */ + @ZenCodeType.Getter("sneaking") + public boolean isSneaking() { + return click.player().isShiftKeyDown(); + } + + /** The clicked face, such as {@code up}. */ + @ZenCodeType.Getter("face") + public String getFace() { + return click.use().face().getSerializedName(); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptClickHandler.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptClickHandler.java new file mode 100644 index 00000000..acc630e5 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptClickHandler.java @@ -0,0 +1,14 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import org.openzen.zencode.java.ZenCodeType; + +/** A script function run when a player right-clicks a block. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ClickHandler") +@FunctionalInterface +public interface ScriptClickHandler { + @ZenCodeType.Method + void handle(ScriptClickContext context); +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java index e93befe7..ffe1e63a 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java @@ -27,8 +27,11 @@ public abstract class ScriptDefinitionProperties { static final String ALL = "all"; + private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); + private final Set tags; private final Map events; + private final Map hooks = new java.util.HashMap<>(); private JsonObject factoryData; ScriptDefinitionProperties( @@ -45,6 +48,39 @@ public abstract class ScriptDefinitionProperties { @ZenCodeType.Method public abstract void build(); + /** The registry or module ID, which names the definition's script callbacks. */ + abstract ResourceLocation definitionId(); + + /** + * Binds a script function to {@code event}: registers a callback {@code /} the first + * time, which runs the latest function given; {@code invoke} hands the event's context to it. A + * function that throws is logged and the game carries on. + */ + void hook( + zone.moddev.mc.lib.api.MaterialEvent event, + H handler, + java.util.function.BiConsumer invoke) { + var id = definitionId().withSuffix("/" + event.name()); + boolean first = !hooks.containsKey(event.name()); + hooks.put(event.name(), java.util.Objects.requireNonNull(handler)); + if (first) + ScriptDefinitions.catalog() + .events() + .registerCallback( + id, + event, + context -> { + @SuppressWarnings("unchecked") + var current = (H) hooks.get(event.name()); + try { + invoke.accept(current, context); + } catch (RuntimeException ex) { + LOGGER.error("Script {} handler of {} failed", event.name(), definitionId(), ex); + } + }); + addEvent(event.name(), id.toString()); + } + /** Settings for the Java implementation, such as {@code {detects: "players"}}. */ @ZenCodeType.Getter("factoryData") public IData getFactoryData() { diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptHitContext.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptHitContext.java new file mode 100644 index 00000000..854b0a6f --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptHitContext.java @@ -0,0 +1,43 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; +import com.blamejared.crafttweaker.api.item.IItemStack; + +import net.minecraft.world.entity.LivingEntity; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.api.MaterialEventContexts.ItemHit; + +/** What an {@code onHit} handler receives: a melee hit that dealt damage. Server side. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.HitContext") +public final class ScriptHitContext { + private final ItemHit hit; + + ScriptHitContext(ItemHit hit) { + this.hit = hit; + } + + @ZenCodeType.Getter("attacker") + public LivingEntity getAttacker() { + return hit.attacker(); + } + + @ZenCodeType.Getter("target") + public LivingEntity getTarget() { + return hit.target(); + } + + /** The item that hit. */ + @ZenCodeType.Getter("stack") + public IItemStack getStack() { + return IItemStack.of(hit.stack()); + } + + /** The damage dealt, after armor and other reductions. */ + @ZenCodeType.Getter("damage") + public float getDamage() { + return hit.damage(); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptHitHandler.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptHitHandler.java new file mode 100644 index 00000000..7d758b4d --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptHitHandler.java @@ -0,0 +1,14 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import org.openzen.zencode.java.ZenCodeType; + +/** A script function run after a melee hit with an item. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.HitHandler") +@FunctionalInterface +public interface ScriptHitHandler { + @ZenCodeType.Method + void handle(ScriptHitContext context); +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java index 7acb6dfc..c790a645 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java @@ -78,6 +78,11 @@ public String getModule() { * @throws IllegalArgumentException if a value is invalid * @throws IllegalStateException if the name was already built */ + @Override + ResourceLocation definitionId() { + return id; + } + @ZenCodeType.Method @Override public void build() { diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemClickContext.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemClickContext.java new file mode 100644 index 00000000..585d8246 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemClickContext.java @@ -0,0 +1,82 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; +import com.blamejared.crafttweaker.api.item.IItemStack; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.api.MaterialEventContexts.AirTarget; +import zone.moddev.mc.lib.api.MaterialEventContexts.BlockTarget; +import zone.moddev.mc.lib.api.MaterialEventContexts.EntityTarget; +import zone.moddev.mc.lib.api.MaterialEventContexts.ToolUse; + +/** + * What an item's {@code onRightClick} handler receives: a right-click with the item on air, a + * block or an entity. Runs on both sides, alongside the item's own use. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ItemClickContext") +public final class ScriptItemClickContext { + private final ToolUse use; + + ScriptItemClickContext(ToolUse use) { + this.use = use; + } + + @ZenCodeType.Getter("player") + public Player getPlayer() { + return use.player(); + } + + @ZenCodeType.Getter("client") + public boolean isClient() { + return use.player().level().isClientSide(); + } + + @ZenCodeType.Getter("stack") + public IItemStack getStack() { + return IItemStack.of(use.target().stack()); + } + + /** What was clicked: {@code air}, {@code block} or {@code entity}. */ + @ZenCodeType.Getter("target") + public String getTarget() { + return switch (use.target()) { + case BlockTarget block -> "block"; + case EntityTarget entity -> "entity"; + case AirTarget air -> "air"; + }; + } + + /** The clicked entity; null unless {@code target} is {@code entity}. */ + @ZenCodeType.Nullable + @ZenCodeType.Getter("entity") + public Entity getEntity() { + return use.target() instanceof EntityTarget target ? target.entity() : null; + } + + /** The clicked block's position; 0 unless {@code target} is {@code block}. */ + @ZenCodeType.Getter("x") + public int getX() { + return use.target() instanceof BlockTarget block ? block.position().getX() : 0; + } + + @ZenCodeType.Getter("y") + public int getY() { + return use.target() instanceof BlockTarget block ? block.position().getY() : 0; + } + + @ZenCodeType.Getter("z") + public int getZ() { + return use.target() instanceof BlockTarget block ? block.position().getZ() : 0; + } + + /** The clicked face; empty unless {@code target} is {@code block}. */ + @ZenCodeType.Getter("face") + public String getFace() { + return use.target() instanceof BlockTarget block ? block.face().getSerializedName() : ""; + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemClickHandler.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemClickHandler.java new file mode 100644 index 00000000..83e983ae --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemClickHandler.java @@ -0,0 +1,14 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import org.openzen.zencode.java.ZenCodeType; + +/** A script function run when a player right-clicks with an item. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ItemClickHandler") +@FunctionalInterface +public interface ScriptItemClickHandler { + @ZenCodeType.Method + void handle(ScriptItemClickContext context); +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java index f705674e..3ab952cb 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java @@ -88,6 +88,11 @@ public void setFactory(String factory) { * * @throws IllegalArgumentException if a value is invalid or the name is taken */ + @Override + ResourceLocation definitionId() { + return id; + } + @ZenCodeType.Method @Override public void build() { diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java index dfd03091..8cf83eb6 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java @@ -9,12 +9,14 @@ import org.openzen.zencode.java.ZenCodeType; +import zone.moddev.mc.lib.api.MaterialEventTypes; import zone.moddev.mc.lib.common.codecs.ItemTemplate; import zone.moddev.mc.lib.common.types.FactoryData; /** - * What a script item and a script item module share: stack size, durability, fire resistance and - * container, besides the tags (item tags), events and factory data of every definition. Values + * What a script item and a script item module share: stack size, durability, fire resistance, + * container, use time and script handlers, besides the tags (item tags), events and factory data of + * every definition. Values * start from the module the builder is based on. */ @ZenRegister(loaders = ScriptDefinitions.LOADER) @@ -25,6 +27,8 @@ public abstract class ScriptItemProperties extends ScriptDefinitionProperties { private int durability; private boolean fireResistant; private Optional container; + private int useDuration; + private String useAnimation; ScriptItemProperties(ItemTemplate base, FactoryData factoryData, Set tags) { super(tags, base.events(), factoryData); @@ -33,6 +37,106 @@ public abstract class ScriptItemProperties extends ScriptDefinitionProperties { durability = base.damage(); fireResistant = base.fireResistant(); container = base.container(); + useDuration = base.delayedUse().map(ItemTemplate.DelayedUse::duration).orElse(0); + useAnimation = + base.delayedUse() + .map(use -> use.animation().name().toLowerCase(java.util.Locale.ROOT)) + .orElse("none"); + } + + /** + * Ticks the item takes to use when right-clicked, like eating or drawing a bow; 0 (the default) + * for an item that is not used over time. Plain items need it for the {@code onUse...} handlers; + * items that are already used over time (food, bows) have their own. + */ + @ZenCodeType.Getter("useDuration") + public int getUseDuration() { + return useDuration; + } + + @ZenCodeType.Setter("useDuration") + public void setUseDuration(int useDuration) { + this.useDuration = useDuration; + } + + /** + * How the player holds the item while using it: {@code none}, {@code eat}, {@code drink}, {@code + * block}, {@code bow}, {@code spear}, {@code crossbow}, {@code spyglass}, {@code toot_horn} or + * {@code brush}. + */ + @ZenCodeType.Getter("useAnimation") + public String getUseAnimation() { + return useAnimation; + } + + @ZenCodeType.Setter("useAnimation") + public void setUseAnimation(String useAnimation) { + net.minecraft.world.item.UseAnim.valueOf(useAnimation.toUpperCase(java.util.Locale.ROOT)); + this.useAnimation = useAnimation; + } + + /** Runs {@code handler} when an entity starts using the item; it can change the duration or cancel. */ + @ZenCodeType.Method + public ScriptItemProperties onUseStart(ScriptItemUseHandler handler) { + return use(MaterialEventTypes.ITEM_USE_START, handler); + } + + /** Runs {@code handler} each tick the item is used; it can cancel. */ + @ZenCodeType.Method + public ScriptItemProperties onUseTick(ScriptItemUseHandler handler) { + return use(MaterialEventTypes.ITEM_USE_TICK, handler); + } + + /** Runs {@code handler} when use stops early, like releasing a bow. */ + @ZenCodeType.Method + public ScriptItemProperties onUseStop(ScriptItemUseHandler handler) { + return use(MaterialEventTypes.ITEM_USE_STOP, handler); + } + + /** Runs {@code handler} when use finishes; it can replace the stack left in hand. */ + @ZenCodeType.Method + public ScriptItemProperties onUseFinish(ScriptItemUseHandler handler) { + return use(MaterialEventTypes.ITEM_USE_FINISH, handler); + } + + private ScriptItemProperties use( + zone.moddev.mc.lib.api.MaterialEvent event, + ScriptItemUseHandler handler) { + hook(event, handler, (h, use) -> h.handle(new ScriptItemUseContext(use))); + return this; + } + + /** Runs {@code handler} on the server after a melee hit with the item has dealt damage. */ + @ZenCodeType.Method + public ScriptItemProperties onHit(ScriptHitHandler handler) { + hook(MaterialEventTypes.ITEM_HIT, handler, (h, hit) -> h.handle(new ScriptHitContext(hit))); + return this; + } + + /** + * Runs {@code handler} on the server when a player holding the item is about to break a block; it + * can cancel, or {@code mine} more blocks (a 3x3 pickaxe). + */ + @ZenCodeType.Method + public ScriptItemProperties onBreakBlock(ScriptBreakHandler handler) { + hook( + MaterialEventTypes.ITEM_BREAK_BLOCK, + handler, + (h, broken) -> h.handle(new ScriptBreakContext(broken))); + return this; + } + + /** + * Runs {@code handler} when a player right-clicks with the item on air, a block or an entity, on + * both sides, alongside the item's own use. + */ + @ZenCodeType.Method + public ScriptItemProperties onRightClick(ScriptItemClickHandler handler) { + hook( + MaterialEventTypes.TOOL_RIGHT_CLICK, + handler, + (h, click) -> h.handle(new ScriptItemClickContext(click))); + return this; } /** How many fit in one stack, 1 to 99. */ @@ -95,7 +199,14 @@ ItemTemplate template() { fireResistant, base.food(), container, - base.delayedUse(), + useDuration > 0 + ? Optional.of( + new ItemTemplate.DelayedUse( + useDuration, + net.minecraft.world.item.UseAnim.valueOf( + useAnimation.toUpperCase(java.util.Locale.ROOT)), + base.delayedUse().flatMap(ItemTemplate.DelayedUse::onComplete))) + : Optional.empty(), base.colorHandler(), base.lore(), base.actions(), diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemUseContext.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemUseContext.java new file mode 100644 index 00000000..48a71ec3 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemUseContext.java @@ -0,0 +1,76 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.Locale; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; +import com.blamejared.crafttweaker.api.item.IItemStack; + +import net.minecraft.world.entity.LivingEntity; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.api.MaterialEventContexts.ItemUse; + +/** + * What {@code onUseStart}, {@code onUseTick}, {@code onUseStop} and {@code onUseFinish} handlers + * receive. Runs on both sides; check {@code client} before changing the world. + */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ItemUseContext") +public final class ScriptItemUseContext { + private final ItemUse use; + + ScriptItemUseContext(ItemUse use) { + this.use = use; + } + + /** {@code start}, {@code tick}, {@code stop} or {@code finish}. */ + @ZenCodeType.Getter("phase") + public String getPhase() { + return use.phase().name().toLowerCase(Locale.ROOT); + } + + /** Who is using the item. */ + @ZenCodeType.Getter("entity") + public LivingEntity getEntity() { + return use.entity(); + } + + @ZenCodeType.Getter("client") + public boolean isClient() { + return use.entity().level().isClientSide(); + } + + @ZenCodeType.Getter("stack") + public IItemStack getStack() { + return IItemStack.of(use.stack()); + } + + /** The total ticks of use at start; the ticks left later. Set it only in {@code onUseStart}. */ + @ZenCodeType.Getter("duration") + public int getDuration() { + return use.duration(); + } + + @ZenCodeType.Setter("duration") + public void setDuration(int duration) { + use.setDuration(duration); + } + + /** The stack left in hand when use finishes. Set it only in {@code onUseFinish}. */ + @ZenCodeType.Getter("result") + public IItemStack getResult() { + return IItemStack.of(use.result()); + } + + @ZenCodeType.Setter("result") + public void setResult(IItemStack result) { + use.setResult(result.getInternal()); + } + + /** Stops the use; only in {@code onUseStart} and {@code onUseTick}. */ + @ZenCodeType.Method + public void cancel() { + use.cancel(); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemUseHandler.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemUseHandler.java new file mode 100644 index 00000000..2447f891 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemUseHandler.java @@ -0,0 +1,14 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import org.openzen.zencode.java.ZenCodeType; + +/** A script function run during the use of an item: start, each tick, stop or finish. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ItemUseHandler") +@FunctionalInterface +public interface ScriptItemUseHandler { + @ZenCodeType.Method + void handle(ScriptItemUseContext context); +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java index 5ff957e6..ebb42bd7 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java @@ -144,7 +144,10 @@ static SortedMap assets( (part, block) -> { var id = block.name(); lang.put("block." + id.getNamespace() + "." + id.getPath(), display(id.getPath())); - var tpl = templatePart(part, "blockstates/", templates, basedOn); + var tpl = + templatePart(part, "blockstates/", templates, basedOn) + // A plain block that turns to face its placer gets front and top faces. + .map(found -> found.equals("block") && directional(part) ? "block_facing" : found); if (tpl.isEmpty()) { noTemplate("block", id, part); return; @@ -183,6 +186,16 @@ static SortedMap assets( return files; } + /** Whether the part's redstone settings turn it to face its placer. */ + private static boolean directional(MaterialPart part) { + var json = part.factoryData().json(); + if (!json.isJsonObject() || !json.getAsJsonObject().has("redstone")) return false; + var redstone = json.getAsJsonObject().get("redstone"); + return redstone.isJsonObject() + && redstone.getAsJsonObject().has("facing") + && redstone.getAsJsonObject().get("facing").getAsBoolean(); + } + private static void noTemplate(String kind, ResourceLocation id, MaterialPart part) { LOGGER.warn( "Script {} {} uses {}, which has no MMDLib template; supply its models and textures in" diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptRedstoneContext.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptRedstoneContext.java new file mode 100644 index 00000000..40443d7b --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptRedstoneContext.java @@ -0,0 +1,37 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import org.openzen.zencode.java.ZenCodeType; + +import zone.moddev.mc.lib.api.MaterialEventContexts.RedstoneChange; + +/** What an {@code onRedstoneChanged} handler receives: the block and the power reaching it. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.RedstoneContext") +public final class ScriptRedstoneContext extends ScriptBlockContext { + private final RedstoneChange change; + + ScriptRedstoneContext(RedstoneChange change) { + super(change.level(), change.position()); + this.change = change; + } + + /** The strongest power reaching the sides the block reads, 0 to 15. */ + @ZenCodeType.Getter("power") + public int getPower() { + return change.power(); + } + + /** That power before this change. */ + @ZenCodeType.Getter("previous") + public int getPrevious() { + return change.previous(); + } + + /** Whether any read side receives power. */ + @ZenCodeType.Getter("powered") + public boolean isPowered() { + return change.powered(); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptRedstoneHandler.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptRedstoneHandler.java new file mode 100644 index 00000000..61b045e2 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptRedstoneHandler.java @@ -0,0 +1,14 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; + +import org.openzen.zencode.java.ZenCodeType; + +/** A script function run when the power reaching a block changes. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.RedstoneHandler") +@FunctionalInterface +public interface ScriptRedstoneHandler { + @ZenCodeType.Method + void handle(ScriptRedstoneContext context); +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___block_facing.json b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___block_facing.json new file mode 100644 index 00000000..a0f6406c --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/blockstates/__material___block_facing.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "mmdlib:block/__material___block_facing" + }, + "facing=east": { + "model": "mmdlib:block/__material___block_facing", + "y": 90 + }, + "facing=south": { + "model": "mmdlib:block/__material___block_facing", + "y": 180 + }, + "facing=west": { + "model": "mmdlib:block/__material___block_facing", + "y": 270 + } + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___block_facing.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___block_facing.json new file mode 100644 index 00000000..edb3efd0 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/block/__material___block_facing.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/orientable", + "textures": { + "front": "mmdlib:block/__material___block_facing_front", + "side": "mmdlib:block/__material___block", + "top": "mmdlib:block/__material___block_facing_top" + } +} diff --git a/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___block_facing.json b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___block_facing.json new file mode 100644 index 00000000..722df6a4 --- /dev/null +++ b/src/main/resources/mmdlib_templates/assets/mmdlib/models/item/__material___block_facing.json @@ -0,0 +1,3 @@ +{ + "parent": "mmdlib:block/__material___block_facing" +} diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ContentSmoke.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ContentSmoke.java index fd1a05d6..6ee8b4ca 100644 --- a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ContentSmoke.java +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ContentSmoke.java @@ -33,14 +33,19 @@ final class ContentSmoke { private ContentSmoke() {} - /** The materials, the standalone detector plate and the item count. */ + /** + * The materials, the standalone blocks (the detector plate; the test-only NAND gate and hook block + * from test resources; the hook block from the test script) and the item count. + */ static void registered() { if (MMDLib.loadedMaterials().size() != 15) throw new AssertionError("Missing materials"); var detector = BuiltInRegistries.BLOCK.get(ResourceLocation.parse("mmdlib:human_detector")); - if (MMDLib.standaloneBlocks().size() != 1 + if (MMDLib.standaloneBlocks().size() != 4 || !(detector instanceof StandardBlocks.DetectorPressurePlate)) throw new AssertionError("Standalone player-only plate missing: " + detector); - if (registrar().items().size() != 234) + // 234 release items; from test resources the gate's and hook block's block items and the hook + // tool; from the test script the hook block's block item, the hook tool and the hammer. + if (registrar().items().size() != 240) throw new AssertionError("Wrong runtime item count: " + registrar().items().size()); } @@ -63,7 +68,8 @@ static void blocksPlace(ServerLevel level, BlockPos floor) { throw new AssertionError("Placement failed: " + entry.getKey() + " result " + result); placed++; } - if (placed != 59) throw new AssertionError("Wrong placed block count: " + placed); + // 59 release blocks, the test gate, the hook block and the script hook block. + if (placed != 62) throw new AssertionError("Wrong placed block count: " + placed); } /** Arrows and bolts keep their pickup item and its components across save and load. */ diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/HookSmoke.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/HookSmoke.java new file mode 100644 index 00000000..9dcb6aa5 --- /dev/null +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/HookSmoke.java @@ -0,0 +1,169 @@ +package zone.moddev.mc.lib.smoke; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +import zone.moddev.mc.lib.api.MaterialEventContexts.BlockAction; +import zone.moddev.mc.lib.api.MaterialEventContexts.BlockBreak; +import zone.moddev.mc.lib.api.MaterialEventContexts.ItemHit; +import zone.moddev.mc.lib.api.MaterialEventContexts.ItemUse; + +/** + * The item and block hooks, bound by test resources: {@code mmdlib:test_hook_tool} (used over 20 + * ticks) binds every item hook and {@code mmdlib:test_hook_block} every block hook to the handlers + * here, through {@code mmdlib/extension/test_hooks.json}. Handlers count their calls and act. + */ +public final class HookSmoke { + private static final Map calls = new ConcurrentHashMap<>(); + + private HookSmoke() {} + + private static void count(String hook) { + calls.merge(hook, 1, Integer::sum); + } + + private static int calls(String hook) { + return calls.getOrDefault(hook, 0); + } + + /** Shortens the use to 5 ticks. */ + public static void useStart(ItemUse use) { + count("use-start"); + use.setDuration(5); + } + + public static void useStop(ItemUse use) { + count("use-stop"); + } + + /** Leaves a diamond in hand. */ + public static void useFinish(ItemUse use) { + count("use-finish"); + use.setResult(new ItemStack(Items.DIAMOND)); + } + + /** Sets the target on fire. */ + public static void hit(ItemHit hit) { + count("hit"); + if (hit.damage() > 0) hit.target().igniteForSeconds(5); + } + + /** Also mines the block above, as a two-high mining tool would. */ + public static void breakBlock(BlockBreak broken) { + count("break-block"); + broken.mine(broken.position().above()); + } + + public static void placed(BlockAction action) { + count("placed"); + } + + public static void broken(BlockBreak broken) { + count("broken"); + } + + public static void steppedOn(BlockAction action) { + count("stepped-on"); + } + + public static void randomTick(BlockAction action) { + count("random-tick"); + } + + private static ItemStack tool() { + return new ItemStack(BuiltInRegistries.ITEM.get(ResourceLocation.parse("mmdlib:test_hook_tool"))); + } + + /** Start, shortened duration, finish with a replaced stack; then a use released early. */ + static void use(GameTestHelper helper) { + var player = helper.makeMockServerPlayerInLevel(); + player.setGameMode(GameType.SURVIVAL); + player.setItemInHand(InteractionHand.MAIN_HAND, tool()); + int starts = calls("use-start"), finishes = calls("use-finish"), stops = calls("use-stop"); + player.getMainHandItem().use(helper.getLevel(), player, InteractionHand.MAIN_HAND); + require(player.isUsingItem() && player.getUseItemRemainingTicks() == 5, "use starts, 5 ticks"); + // A server player's living update runs in doTick, which its connection normally drives. + for (int i = 0; i < 6 && player.isUsingItem(); i++) player.doTick(); + require(calls("use-start") == starts + 1 && calls("use-finish") == finishes + 1, "use finishes"); + require(player.getMainHandItem().is(Items.DIAMOND), "the finish handler replaced the stack"); + player.setItemInHand(InteractionHand.MAIN_HAND, tool()); + player.getMainHandItem().use(helper.getLevel(), player, InteractionHand.MAIN_HAND); + player.releaseUsingItem(); + require(calls("use-stop") == stops + 1, "releasing early stops the use"); + helper.succeed(); + } + + /** A melee hit with the tool sets the target on fire. */ + static void hit(GameTestHelper helper, BlockPos center) { + var player = helper.makeMockServerPlayerInLevel(); + player.setGameMode(GameType.SURVIVAL); + player.setItemInHand(InteractionHand.MAIN_HAND, tool()); + var target = helper.spawnWithNoFreeWill(EntityType.PIG, center); + int hits = calls("hit"); + player.attack(target); + require(calls("hit") == hits + 1, "the hit was reported"); + require(target.getRemainingFireTicks() > 0, "the hit handler set the target on fire"); + helper.succeed(); + } + + /** Breaking one block with the tool also mines the block above, reported once. */ + static void mining(GameTestHelper helper, BlockPos center) { + var level = helper.getLevel(); + var pos = helper.absolutePos(center); + level.setBlock(pos, Blocks.DIRT.defaultBlockState(), 3); + level.setBlock(pos.above(), Blocks.DIRT.defaultBlockState(), 3); + var player = helper.makeMockServerPlayerInLevel(); + player.setGameMode(GameType.SURVIVAL); + player.setItemInHand(InteractionHand.MAIN_HAND, tool()); + int breaks = calls("break-block"); + require(player.gameMode.destroyBlock(pos), "the block breaks"); + require(level.getBlockState(pos).isAir() && level.getBlockState(pos.above()).isAir(), "both mined"); + require(calls("break-block") == breaks + 1, "mine() does not report its own breaks again"); + helper.succeed(); + } + + /** Placing, stepping on, random ticks and breaking the hook block are each reported. */ + static void block(GameTestHelper helper, BlockPos center) { + var level = helper.getLevel(); + var floor = helper.absolutePos(center); + level.setBlock(floor, Blocks.STONE.defaultBlockState(), 3); + var pos = floor.above(); + var player = helper.makeMockServerPlayerInLevel(); + player.setGameMode(GameType.SURVIVAL); + var item = BuiltInRegistries.ITEM.get(ResourceLocation.parse("mmdlib:test_hook_block")); + player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(item)); + int placed = calls("placed"), stepped = calls("stepped-on"), ticks = calls("random-tick"), broken = calls("broken"); + var hit = new BlockHitResult(Vec3.atCenterOf(floor).add(0, .5, 0), Direction.UP, floor, false); + player.getMainHandItem().useOn(new UseOnContext(player, InteractionHand.MAIN_HAND, hit)); + var state = level.getBlockState(pos); + require(state.getBlock() != Blocks.AIR && calls("placed") == placed + 1, "placing is reported"); + state.getBlock().stepOn(level, pos, state, player); + require(calls("stepped-on") == stepped + 1, "stepping on is reported"); + require(state.isRandomlyTicking(), "the block takes random ticks"); + state.randomTick(level, pos, level.getRandom()); + require(calls("random-tick") == ticks + 1, "random ticks are reported"); + player.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY); + require(player.gameMode.destroyBlock(pos), "the block breaks"); + require(calls("broken") == broken + 1, "breaking is reported"); + helper.succeed(); + } + + private static void require(boolean condition, String what) { + if (!condition) throw new AssertionError(what); + } +} diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java index 2a3087ae..bd1bcb97 100644 --- a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java @@ -113,6 +113,46 @@ public static void toolsAndArmor(GameTestHelper helper) throws Exception { helper.succeed(); } + @GameTest(template = EMPTY) + public static void itemUseHooks(GameTestHelper helper) { + HookSmoke.use(helper); + } + + @GameTest(template = EMPTY) + public static void itemHitHook(GameTestHelper helper) { + HookSmoke.hit(helper, CENTER); + } + + @GameTest(template = EMPTY) + public static void miningHook(GameTestHelper helper) { + HookSmoke.mining(helper, CENTER); + } + + @GameTest(template = EMPTY) + public static void blockHooks(GameTestHelper helper) { + HookSmoke.block(helper, CENTER); + } + + @GameTest(template = EMPTY) + public static void scriptItemHooks(GameTestHelper helper) { + ScriptHookSmoke.item(helper, CENTER); + } + + @GameTest(template = EMPTY) + public static void scriptHammer(GameTestHelper helper) { + ScriptHookSmoke.hammer(helper, CENTER); + } + + @GameTest(template = EMPTY) + public static void scriptBlockHooks(GameTestHelper helper) { + ScriptHookSmoke.block(helper, CENTER); + } + + @GameTest(template = EMPTY) + public static void redstoneGate(GameTestHelper helper) { + RedstoneSmoke.gate(helper, CENTER); + } + @GameTest(template = EMPTY) public static void capabilities(GameTestHelper helper) { CapabilitySmoke.check(helper.getLevel()); diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/RedstoneSmoke.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/RedstoneSmoke.java new file mode 100644 index 00000000..4ae59227 --- /dev/null +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/RedstoneSmoke.java @@ -0,0 +1,68 @@ +package zone.moddev.mc.lib.smoke; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Blocks; + +import zone.moddev.mc.lib.api.MaterialEventContexts.RedstoneChange; +import zone.moddev.mc.lib.common.runtime.RedstoneBlock; + +/** + * A directional, delayed redstone gate built from definition data: {@code mmdlib:test_nand_gate} + * (test resources) reads only its left and right sides and powers only its front, two ticks after + * its inputs change. + */ +public final class RedstoneSmoke { + private RedstoneSmoke() {} + + /** The gate's {@code redstone-changed} handler: off only while both sides are powered. */ + public static void nand(RedstoneChange change) { + var block = (RedstoneBlock) change.state().getBlock(); + boolean left = change.signal(block.toWorld(change.state(), Direction.WEST)) > 0; + boolean right = change.signal(block.toWorld(change.state(), Direction.EAST)) > 0; + RedstoneBlock.setOutput(change.level(), change.position(), left && right ? 0 : 15); + } + + /** Places the gate facing east and drives its sides through every input combination. */ + static void gate(GameTestHelper helper, BlockPos center) { + var level = helper.getLevel(); + var pos = helper.absolutePos(center); + var gate = + (RedstoneBlock) BuiltInRegistries.BLOCK.get(ResourceLocation.parse("mmdlib:test_nand_gate")); + level.setBlock(pos, gate.defaultBlockState().setValue(RedstoneBlock.FACING, Direction.EAST), 3); + // Facing east: the front is east, the left north, the right south and the back west. + var left = pos.north(); + var right = pos.south(); + var back = pos.west(); + helper + .startSequence() + .thenExecuteAfter(3, () -> expect(helper, pos, 15, "unpowered gate is on")) + .thenExecute(() -> level.setBlock(back, Blocks.REDSTONE_BLOCK.defaultBlockState(), 3)) + .thenExecuteAfter(3, () -> expect(helper, pos, 15, "the back side is not read")) + .thenExecute(() -> level.setBlock(left, Blocks.REDSTONE_BLOCK.defaultBlockState(), 3)) + .thenExecuteAfter(3, () -> expect(helper, pos, 15, "one input keeps it on")) + .thenExecute(() -> level.setBlock(right, Blocks.REDSTONE_BLOCK.defaultBlockState(), 3)) + .thenExecute(() -> expect(helper, pos, 15, "the change waits for the delay")) + .thenExecuteAfter(3, () -> expect(helper, pos, 0, "both inputs turn it off")) + .thenExecute(() -> level.setBlock(left, Blocks.AIR.defaultBlockState(), 3)) + .thenExecuteAfter(3, () -> expect(helper, pos, 15, "dropping one input turns it on")) + .thenSucceed(); + } + + private static void expect(GameTestHelper helper, BlockPos pos, int output, String what) { + var level = helper.getLevel(); + var state = level.getBlockState(pos); + int stored = state.getValue(RedstoneBlock.OUTPUT); + // getSignal's direction points from the asking neighbor to the gate. + int front = state.getSignal(level, pos, Direction.WEST); + int back = state.getSignal(level, pos, Direction.EAST); + int side = state.getSignal(level, pos, Direction.SOUTH); + if (stored != output || front != output || back != 0 || side != 0) + throw new AssertionError( + what + ": expected front " + output + ", got stored " + stored + ", front " + front + + ", back " + back + ", side " + side); + } +} diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ScriptHookSmoke.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ScriptHookSmoke.java new file mode 100644 index 00000000..d91c004d --- /dev/null +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ScriptHookSmoke.java @@ -0,0 +1,130 @@ +package zone.moddev.mc.lib.smoke; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +import zone.moddev.mc.lib.common.runtime.RedstoneBlock; + +/** + * The hooks as CraftTweaker scripts see them: {@code src/runtimeTest/scripts/mmdlib_hooks.zs} + * builds {@code mmdlib:script_hook_tool} and {@code mmdlib:script_hook_block}, whose handlers leave + * marks (fire ticks, mined blocks, redstone output) that these checks look for. + */ +final class ScriptHookSmoke { + private ScriptHookSmoke() {} + + private static ItemStack stack(String id) { + return new ItemStack(BuiltInRegistries.ITEM.get(ResourceLocation.parse(id))); + } + + private static ServerPlayer player(GameTestHelper helper, ItemStack held) { + var player = helper.makeMockServerPlayerInLevel(); + player.setGameMode(GameType.SURVIVAL); + player.setItemInHand(InteractionHand.MAIN_HAND, held); + return player; + } + + /** Right-click on air, use start (duration 10 cut to 3) and finish. */ + static void item(GameTestHelper helper, BlockPos center) { + var level = helper.getLevel(); + var player = player(helper, stack("mmdlib:script_hook_tool")); + player.gameMode.useItem(player, level, player.getMainHandItem(), InteractionHand.MAIN_HAND); + require(player.getRemainingFireTicks() == 33, "onRightClick on air ran"); + require(player.isUsingItem() && player.getUseItemRemainingTicks() == 3, "onUseStart set 3 ticks"); + for (int i = 0; i < 4 && player.isUsingItem(); i++) player.doTick(); + require(!player.isUsingItem() && player.getRemainingFireTicks() >= 70, "onUseFinish ran"); + + var target = helper.spawnWithNoFreeWill(EntityType.PIG, center); + player.attack(target); + require(target.getRemainingFireTicks() == 100, "onHit ran"); + + var pos = helper.absolutePos(center).north(2); + level.setBlock(pos, Blocks.DIRT.defaultBlockState(), 3); + level.setBlock(pos.above(), Blocks.DIRT.defaultBlockState(), 3); + require(player.gameMode.destroyBlock(pos), "the block breaks"); + require(level.getBlockState(pos.above()).isAir(), "onBreakBlock mined the block above"); + helper.succeed(); + } + + /** Placed, right-clicked, stepped on, randomly ticked, and kept or broken. */ + static void block(GameTestHelper helper, BlockPos center) { + var level = helper.getLevel(); + var floor = helper.absolutePos(center); + level.setBlock(floor, Blocks.STONE.defaultBlockState(), 3); + var pos = floor.above(); + var player = player(helper, stack("mmdlib:script_hook_block")); + var top = new BlockHitResult(Vec3.atCenterOf(floor).add(0, .5, 0), Direction.UP, floor, false); + player.getMainHandItem().useOn(new UseOnContext(player, InteractionHand.MAIN_HAND, top)); + require(output(helper, pos) == 4, "onPlaced set the output to 4"); + + player.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY); + var side = new BlockHitResult(Vec3.atCenterOf(pos), Direction.NORTH, pos, false); + player.gameMode.useItemOn(player, level, ItemStack.EMPTY, InteractionHand.MAIN_HAND, side); + require(output(helper, pos) == 5, "onRightClick raised the output"); + + var state = level.getBlockState(pos); + state.getBlock().stepOn(level, pos, state, player); + require(player.getRemainingFireTicks() == 60, "onSteppedOn ran"); + state.randomTick(level, pos, level.getRandom()); + require(output(helper, pos) == 9, "onRandomTick set the output to 9"); + + player.setShiftKeyDown(true); + player.gameMode.destroyBlock(pos); + require(!level.getBlockState(pos).isAir(), "onBroken cancelled a sneaking break"); + player.setShiftKeyDown(false); + require(player.gameMode.destroyBlock(pos) && level.getBlockState(pos).isAir(), "it breaks"); + helper.succeed(); + } + + /** + * Mining a wall of dirt face-on with the hammer breaks the 3x3 square around the block, each + * block once: nine dirt items, and a block behind the wall is untouched. + */ + static void hammer(GameTestHelper helper, BlockPos center) { + var level = helper.getLevel(); + var middle = helper.absolutePos(center).above(); + for (int dx = -1; dx <= 1; dx++) + for (int dy = -1; dy <= 1; dy++) + level.setBlock(middle.offset(dx, dy, 0), Blocks.DIRT.defaultBlockState(), 3); + level.setBlock(middle.north(), Blocks.DIRT.defaultBlockState(), 3); + var player = player(helper, stack("mmdlib:script_hammer")); + // Stand south of the wall, looking north at its middle block, so the mined face is south. + player.moveTo(middle.getX() + .5, middle.getY() - 1.0, middle.getZ() + 2.5, 180, 0); + // A living entity looks along its head rotation, which moveTo leaves alone. + player.setYHeadRot(180); + player.yHeadRotO = 180; + require(player.gameMode.destroyBlock(middle), "the middle block breaks"); + var left = new java.util.ArrayList(); + for (int dx = -1; dx <= 1; dx++) + for (int dy = -1; dy <= 1; dy++) + if (!level.getBlockState(middle.offset(dx, dy, 0)).isAir()) left.add(dx + "," + dy); + require(left.isEmpty(), "the 3x3 square is mined; left " + left); + require(!level.getBlockState(middle.north()).isAir(), "the block behind is kept"); + var drops = + level.getEntitiesOfClass( + net.minecraft.world.entity.item.ItemEntity.class, new net.minecraft.world.phys.AABB(middle).inflate(3)); + int dirt = drops.stream().mapToInt(item -> item.getItem().getCount()).sum(); + require(dirt == 9, "each block dropped once, got " + dirt); + helper.succeed(); + } + + private static int output(GameTestHelper helper, BlockPos pos) { + return helper.getLevel().getBlockState(pos).getValue(RedstoneBlock.OUTPUT); + } + + private static void require(boolean condition, String what) { + if (!condition) throw new AssertionError(what); + } +} diff --git a/src/runtimeTest/resources/mmdlib/extension/test_hooks.json b/src/runtimeTest/resources/mmdlib/extension/test_hooks.json new file mode 100644 index 00000000..a555b8ca --- /dev/null +++ b/src/runtimeTest/resources/mmdlib/extension/test_hooks.json @@ -0,0 +1,58 @@ +{ + "events": [ + { + "type": "item-use-start", + "handler": "zone.moddev.mc.lib.smoke.HookSmoke#useStart", + "id": "mmdlib:test_use_start", + "name": "Game test hook item-use-start" + }, + { + "type": "item-use-stop", + "handler": "zone.moddev.mc.lib.smoke.HookSmoke#useStop", + "id": "mmdlib:test_use_stop", + "name": "Game test hook item-use-stop" + }, + { + "type": "item-use-finish", + "handler": "zone.moddev.mc.lib.smoke.HookSmoke#useFinish", + "id": "mmdlib:test_use_finish", + "name": "Game test hook item-use-finish" + }, + { + "type": "item-hit", + "handler": "zone.moddev.mc.lib.smoke.HookSmoke#hit", + "id": "mmdlib:test_hit", + "name": "Game test hook item-hit" + }, + { + "type": "item-break-block", + "handler": "zone.moddev.mc.lib.smoke.HookSmoke#breakBlock", + "id": "mmdlib:test_break_block", + "name": "Game test hook item-break-block" + }, + { + "type": "block-placed", + "handler": "zone.moddev.mc.lib.smoke.HookSmoke#placed", + "id": "mmdlib:test_placed", + "name": "Game test hook block-placed" + }, + { + "type": "block-broken", + "handler": "zone.moddev.mc.lib.smoke.HookSmoke#broken", + "id": "mmdlib:test_broken", + "name": "Game test hook block-broken" + }, + { + "type": "block-stepped-on", + "handler": "zone.moddev.mc.lib.smoke.HookSmoke#steppedOn", + "id": "mmdlib:test_stepped_on", + "name": "Game test hook block-stepped-on" + }, + { + "type": "block-random-tick", + "handler": "zone.moddev.mc.lib.smoke.HookSmoke#randomTick", + "id": "mmdlib:test_random_tick", + "name": "Game test hook block-random-tick" + } + ] +} diff --git a/src/runtimeTest/resources/mmdlib/extension/test_redstone.json b/src/runtimeTest/resources/mmdlib/extension/test_redstone.json new file mode 100644 index 00000000..63ea9792 --- /dev/null +++ b/src/runtimeTest/resources/mmdlib/extension/test_redstone.json @@ -0,0 +1,10 @@ +{ + "events": [ + { + "type": "redstone-changed", + "handler": "zone.moddev.mc.lib.smoke.RedstoneSmoke#nand", + "id": "mmdlib:test_nand_gate", + "name": "Game test NAND gate" + } + ] +} diff --git a/src/runtimeTest/resources/mmdlib/standalone/block/test_hook_block.json b/src/runtimeTest/resources/mmdlib/standalone/block/test_hook_block.json new file mode 100644 index 00000000..7f1a9c5f --- /dev/null +++ b/src/runtimeTest/resources/mmdlib/standalone/block/test_hook_block.json @@ -0,0 +1,12 @@ +{ + "module": "mmdlib:block", + "template": { + "properties": { "random_ticks": true }, + "events": { + "block-placed": "mmdlib:test_placed", + "block-broken": "mmdlib:test_broken", + "block-stepped-on": "mmdlib:test_stepped_on", + "block-random-tick": "mmdlib:test_random_tick" + } + } +} diff --git a/src/runtimeTest/resources/mmdlib/standalone/block/test_nand_gate.json b/src/runtimeTest/resources/mmdlib/standalone/block/test_nand_gate.json new file mode 100644 index 00000000..43eb0680 --- /dev/null +++ b/src/runtimeTest/resources/mmdlib/standalone/block/test_nand_gate.json @@ -0,0 +1,14 @@ +{ + "module": "mmdlib:block", + "factory_data": { + "redstone": { + "input": true, + "output": "state", + "facing": true, + "faces": ["front"], + "inputs": ["left", "right"], + "delay": 2 + } + }, + "template": { "events": { "redstone-changed": "mmdlib:test_nand_gate" } } +} diff --git a/src/runtimeTest/resources/mmdlib/standalone/item/test_hook_tool.json b/src/runtimeTest/resources/mmdlib/standalone/item/test_hook_tool.json new file mode 100644 index 00000000..b15595ad --- /dev/null +++ b/src/runtimeTest/resources/mmdlib/standalone/item/test_hook_tool.json @@ -0,0 +1,14 @@ +{ + "module": "mmdlib:ingot", + "template": { + "max_stack_size": 1, + "delayed_use": { "duration": 20, "animation": "bow" }, + "events": { + "item-use-start": "mmdlib:test_use_start", + "item-use-stop": "mmdlib:test_use_stop", + "item-use-finish": "mmdlib:test_use_finish", + "item-hit": "mmdlib:test_hit", + "item-break-block": "mmdlib:test_break_block" + } + } +} diff --git a/src/runtimeTest/scripts/mmdlib_hooks.zs b/src/runtimeTest/scripts/mmdlib_hooks.zs new file mode 100644 index 00000000..93776d32 --- /dev/null +++ b/src/runtimeTest/scripts/mmdlib_hooks.zs @@ -0,0 +1,54 @@ +#loader mmdlib +// Game-test content for the script hooks (ScriptHookSmoke). Each handler leaves a mark the test +// checks: fire ticks on an entity, an extra mined block, a kept block or a redstone output. +import mods.mmdlib.ItemBuilder; +import mods.mmdlib.BlockBuilder; +import mods.mmdlib.ItemUseContext; +import mods.mmdlib.HitContext; +import mods.mmdlib.BreakContext; +import mods.mmdlib.BlockEventContext; +import mods.mmdlib.ItemClickContext; +import mods.mmdlib.ClickContext; + +var tool = new ItemBuilder("script_hook_tool"); +tool.stackSize = 1; +tool.useDuration = 10; +tool.useAnimation = "bow"; +tool.onUseStart((ctx as ItemUseContext) => { ctx.duration = 3; }); +tool.onUseFinish((ctx as ItemUseContext) => { ctx.entity.setRemainingFireTicks(77); }); +tool.onRightClick((ctx as ItemClickContext) => { + if (ctx.target == "air") ctx.player.setRemainingFireTicks(33); +}); +tool.onHit((ctx as HitContext) => { ctx.target.setRemainingFireTicks(100); }); +tool.onBreakBlock((ctx as BreakContext) => { ctx.mine(ctx.x, ctx.y + 1, ctx.z); }); +tool.build(); + +var block = new BlockBuilder("script_hook_block"); +block.redstoneOutput = true; +block.onPlaced((ctx as BlockEventContext) => { ctx.output = 4; }); +block.onSteppedOn((ctx as BlockEventContext) => { ctx.entity.setRemainingFireTicks(60); }); +block.onRandomTick((ctx as BlockEventContext) => { ctx.output = 9; }); +block.onRightClick((ctx as ClickContext) => { ctx.output = ctx.output + 1; }); +block.onBroken((ctx as BreakContext) => { + if (ctx.player.isShiftKeyDown()) ctx.cancel(); +}); +block.build(); + +// The 3x3 hammer from docs/crafttweaker.md, checked by ScriptHookSmoke.hammer. +var hammer = new ItemBuilder("script_hammer"); +hammer.durability = 500; +hammer.onBreakBlock((ctx as BreakContext) => { + if (ctx.player.isShiftKeyDown()) return; + for a in -1 .. 2 { + for b in -1 .. 2 { + if (ctx.face == "up" || ctx.face == "down") { + ctx.mine(ctx.x + a, ctx.y, ctx.z + b); + } else if (ctx.face == "north" || ctx.face == "south") { + ctx.mine(ctx.x + a, ctx.y + b, ctx.z); + } else { + ctx.mine(ctx.x, ctx.y + a, ctx.z + b); + } + } + } +}); +hammer.build(); diff --git a/src/test/java/zone/moddev/mc/lib/common/builders/RuntimeBehaviorTest.java b/src/test/java/zone/moddev/mc/lib/common/builders/RuntimeBehaviorTest.java index 1b2d2eb3..d9204092 100644 --- a/src/test/java/zone/moddev/mc/lib/common/builders/RuntimeBehaviorTest.java +++ b/src/test/java/zone/moddev/mc/lib/common/builders/RuntimeBehaviorTest.java @@ -192,9 +192,42 @@ void dispatchesRuntimeBehaviors() throws Exception { redstone.apply("{\"output\":\"state\",\"comparator\":\"output\"}")) .orElseThrow() .stateOutput()); + var gate = + RedstoneBlock.Settings.of( + redstone.apply( + "{\"input\":true,\"output\":\"state\",\"facing\":true,\"faces\":[\"front\"]," + + "\"inputs\":[\"left\",\"right\",\"back\"],\"delay\":2}")) + .orElseThrow(); + require( + gate.facing() + && gate.delay() == 2 + && gate.perFace() + && gate.faces().equals(java.util.Set.of(net.minecraft.core.Direction.NORTH)) + && gate.inputs() + .equals( + java.util.Set.of( + net.minecraft.core.Direction.WEST, + net.minecraft.core.Direction.EAST, + net.minecraft.core.Direction.SOUTH))); + require(!lamp.perFace() && !lamp.facing() && lamp.delay() == 0); + // Facing east, the front is east and the left is north; up and down never turn. + var north = net.minecraft.core.Direction.NORTH; + var east = net.minecraft.core.Direction.EAST; + var west = net.minecraft.core.Direction.WEST; + require(RedstoneBlock.rotate(north, east) == east); + require(RedstoneBlock.rotate(west, east) == north); + require(RedstoneBlock.rotate(net.minecraft.core.Direction.UP, east) == net.minecraft.core.Direction.UP); + for (var face : net.minecraft.core.Direction.values()) + for (var facing : net.minecraft.core.Direction.Plane.HORIZONTAL) + require(RedstoneBlock.unrotate(RedstoneBlock.rotate(face, facing), facing) == face); for (String bad : java.util.List.of( "{\"output\":16}", + "{\"faces\":[\"front\"]}", + "{\"inputs\":[\"north\"]}", + "{\"input\":true,\"inputs\":[]}", + "{\"input\":true,\"delay\":-1}", + "{\"delay\":2}", "{\"output\":\"loud\"}", "{\"faces\":[\"sideways\"]}", "{\"comparator\":\"input\"}", diff --git a/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java index 3a453714..0c2e518a 100644 --- a/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java +++ b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java @@ -161,6 +161,32 @@ void materialPartsFromNewModulesUseTheirFactoryTemplate() { require(text(files, "assets/mmdlib/lang/en_us.json").contains("\"Tin Test Steps\""), "name"); } + @Test + void directionalRedstoneBlocksTurnToFaceTheirPlacer() { + var block = net.minecraft.resources.ResourceLocation.parse("mmdlib:block"); + var gate = + new zone.moddev.mc.lib.common.types.StandaloneBlock( + block, + new zone.moddev.mc.lib.common.types.FactoryData( + JsonParser.parseString("{\"redstone\":{\"facing\":true,\"output\":\"state\"}}")), + Optional.empty(), + zone.moddev.mc.lib.common.types.MaterialRuntimeSettings.DEFAULT) + .resolve( + net.minecraft.resources.ResourceLocation.parse("mmdlib:nand_gate"), + MaterialExtensions.GLOBAL.blockModule(block)); + var files = + ScriptMaterialResources.assets( + List.of(), List.of(gate), List.of(), Map.of(), new MaterialTemplates(List.of(TEMPLATES)), TEMPLATES); + var state = text(files, "assets/mmdlib/blockstates/nand_gate.json"); + require(state.contains("facing=east") && state.contains("\"y\": 90"), "turned variants"); + var model = text(files, "assets/mmdlib/models/block/nand_gate.json"); + require( + model.contains("mmdlib:block/nand_gate_front") + && model.contains("mmdlib:block/nand_gate_top") + && model.contains("\"side\": \"mmdlib:block/nand_gate\""), + "front, top and side textures"); + } + private static MaterialFactory.Result item(String id, String module) { var rl = net.minecraft.resources.ResourceLocation.parse(module); return new zone.moddev.mc.lib.common.types.StandaloneItem( From c29fb49773082a94347bb3865d86cb3e22b9019a Mon Sep 17 00:00:00 2001 From: Daniel Hazelton Date: Sat, 7 Nov 2026 15:16:30 -0500 Subject: [PATCH 5/9] Expand CraftTweaker APIs through existing MMDLib runtime paths Bring script-defined content closer to ContentTweaker's builder capabilities without adding parallel script-only factories or extending core schemas and runtime behavior. Preserve existing script syntax, registry identities, and optional CraftTweaker loading. Expose material repair and harvest settings, block runtime overrides, named tool-stat methods, and typed generic-tool configuration. Add typed item, block, module, and block-tag references that retain existing resolution timing. Add module configuration snapshots with independent callback IDs and handler maps, while retaining shared captured script variables and explicit external callbacks. Fix item-module tag lookup so builders can inherit tags from modules registered earlier in the same startup script run. Add fluent item/block aliases, generated English names, and shape-aware default, self, empty, and referenced loot controls. Keep resource metadata outside core definitions, preserve double-slab drops, and retain external pack precedence. Document the exposed API, runtime consumers, parity scope, executable examples, and verification results. Record food and creative-tab follow-up TODOs for shared Java/JSON/CraftTweaker support. Keep per-tool durability scaling explicitly deferred: vanilla TieredItem replaces the factory's calculated durability with the material tier's uses; material durability and speed scaling work. Add focused unit and server fixtures plus isolated real-client smoke runs with CraftTweaker present and absent. Include fixture texture provenance and keep smoke content out of release artifacts. Validation: - ./gradlew --offline check build: passed; 190 unit tests, 85 MMDLib GameTests, 52 companion GameTests, static assets, and packaging checks. - Real integrated-client launches with and without CraftTweaker: passed using llvmpipe under Xvfb after native WSL graphics startup failed. - Inspected rendered names/items/material textures and four directional blocks. - Inspected release JAR: integration classes present; smoke content absent. - git diff --check: passed. --- CHANGELOG.md | 14 + docs/crafttweaker-api-inventory.md | 564 ++++++++++++++++++ docs/crafttweaker-parity-plan.md | 183 ++++++ docs/crafttweaker-verification.md | 76 +++ docs/crafttweaker.md | 256 +++++++- docs/roadmap.md | 45 ++ gradle/verification.gradle | 1 + scripts/crafttweaker-smoke.init.gradle | 28 + src/clientSmoke/README.md | 25 + .../mc/lib/smoke/CraftTweakerClientSmoke.java | 144 +++++ src/clientSmoke/scripts/content.zs | 22 + .../mmdlib/textures/block/client_gate.png | Bin 0 -> 329 bytes .../textures/block/client_gate_front.png | Bin 0 -> 254 bytes .../mmdlib/textures/block/client_gate_top.png | Bin 0 -> 215 bytes .../mmdlib/textures/item/client_widget.png | Bin 0 -> 305 bytes .../compat/CraftTweaker/MaterialPacks.java | 4 +- .../CraftTweaker/ScriptBlockBuilder.java | 15 + .../CraftTweaker/ScriptBlockModule.java | 26 + .../ScriptBlockModuleReference.java | 18 + .../CraftTweaker/ScriptBlockProperties.java | 63 ++ .../CraftTweaker/ScriptBlockReference.java | 18 + .../CraftTweaker/ScriptBlockTagReference.java | 18 + .../ScriptDefinitionProperties.java | 21 + .../CraftTweaker/ScriptDefinitions.java | 19 +- .../CraftTweaker/ScriptItemBuilder.java | 10 + .../compat/CraftTweaker/ScriptItemModule.java | 26 + .../ScriptItemModuleReference.java | 18 + .../CraftTweaker/ScriptItemProperties.java | 33 + .../CraftTweaker/ScriptItemReference.java | 18 + .../CraftTweaker/ScriptMaterialBuilder.java | 141 ++++- .../CraftTweaker/ScriptMaterialResources.java | 57 +- .../CraftTweaker/ScriptResourceOptions.java | 29 + .../CraftTweaker/ScriptToolSettings.java | 83 +++ .../moddev/mc/lib/smoke/ContentSmoke.java | 10 +- .../moddev/mc/lib/smoke/MMDLibGameTests.java | 15 + .../moddev/mc/lib/smoke/ScriptHookSmoke.java | 95 +++ src/runtimeTest/scripts/mmdlib_references.zs | 51 ++ src/runtimeTest/scripts/mmdlib_resources.zs | 29 + src/runtimeTest/scripts/mmdlib_tools.zs | 29 + .../loot_table/blocks/phase4_override.json | 4 + .../ScriptFluentPropertiesTest.java | 54 ++ .../ScriptMaterialResourcesTest.java | 22 + .../CraftTweaker/ScriptReferencesTest.java | 26 + .../CraftTweaker/ScriptToolSettingsTest.java | 35 ++ 44 files changed, 2317 insertions(+), 28 deletions(-) create mode 100644 docs/crafttweaker-api-inventory.md create mode 100644 docs/crafttweaker-parity-plan.md create mode 100644 docs/crafttweaker-verification.md create mode 100644 scripts/crafttweaker-smoke.init.gradle create mode 100644 src/clientSmoke/README.md create mode 100644 src/clientSmoke/java/zone/moddev/mc/lib/smoke/CraftTweakerClientSmoke.java create mode 100644 src/clientSmoke/scripts/content.zs create mode 100644 src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate.png create mode 100644 src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate_front.png create mode 100644 src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate_top.png create mode 100644 src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/item/client_widget.png create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModuleReference.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockReference.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockTagReference.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModuleReference.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemReference.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptResourceOptions.java create mode 100644 src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptToolSettings.java create mode 100644 src/runtimeTest/scripts/mmdlib_references.zs create mode 100644 src/runtimeTest/scripts/mmdlib_resources.zs create mode 100644 src/runtimeTest/scripts/mmdlib_tools.zs create mode 100644 src/runtimeTest/scripts/mmdlibdata/data/mmdlib/loot_table/blocks/phase4_override.json create mode 100644 src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptFluentPropertiesTest.java create mode 100644 src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptReferencesTest.java create mode 100644 src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptToolSettingsTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index dbf83f33..60f805b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,3 +21,17 @@ Changes on `crafttweaker-integration` since `master-2.0-1.21.1-neo`. - Document the scripting API, generated resources, startup ordering, and current limitations in [CraftTweaker definitions](docs/crafttweaker.md) and [definition loading](docs/definition-loading.md). + ++- Expose material repair/harvest settings, block runtime overrides, and named tool ++ stat methods to CraftTweaker. Add a typed helper for existing generic-tool factory ++ data, with script integration tests and an API/runtime capability inventory. ++ Per-tool durability scaling remains limited by vanilla TieredItem resetting the ++ computed durability to the material tier's uses; material durability works. +- Add typed item/block/tag/module ID references and independent module snapshots + for CraftTweaker. Snapshot copies rebind locally owned handlers; module tags are + now available to subsequent builders during the same script run. +- Add fluent item/block builder conveniences, generated English name overrides, + and default/self/empty/referenced loot controls. Existing external resource + overrides retain priority; shape-aware default loot remains supported. +- Add reproducible isolated client-smoke runs with CraftTweaker present and absent, + generated-content rendering captures, and a verification report for the parity work. diff --git a/docs/crafttweaker-api-inventory.md b/docs/crafttweaker-api-inventory.md new file mode 100644 index 00000000..2d0f1d50 --- /dev/null +++ b/docs/crafttweaker-api-inventory.md @@ -0,0 +1,564 @@ +# CraftTweaker API inventory and runtime support + +Updated for phases 2–4 on top of MMDLib `1862428`. Source support and runtime test evidence are distinguished below. All script names are under `mods.mmdlib`. + +## Declared API and inheritance + +Members are listed on their declaring type. A child also exposes its listed parent members. `get` and `set` denote property access; constructors use the script type name. Unannotated Java helpers are excluded. + +### `BlockBuilder` + +Source: [ScriptBlockBuilder](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockBuilder.java). Inherits `BlockProperties`. + +- constructor `BlockBuilder(String name)` +- constructor `BlockBuilder(String name, String module)` +- constructor `BlockBuilder(String name, BlockModuleReference module)` +- get `reference` — `BlockReference getReference()` +- get `name` — `String getName()` +- get `module` — `String getModule()` +- method `BlockBuilder baseBlockFrom(BlockReference block)` +- get `baseBlock` — `String getBaseBlock()` +- set `baseBlock` — `void setBaseBlock(String baseBlock)` +- get `blockSetType` — `String getBlockSetType()` +- set `blockSetType` — `void setBlockSetType(String blockSetType)` +- set `blockEntityData` — `void setBlockEntityData(IData data)` +- method `void build()` + +### `BlockContext` + +Source: [ScriptBlockContext](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockContext.java). + +- get `client` — `boolean isClient()` +- get `x` — `int getX()` +- get `y` — `int getY()` +- get `z` — `int getZ()` +- get `facing` — `String getFacing()` +- method `int input(String side)` +- get `output` — `int getOutput()` +- set `output` — `void setOutput(int power)` + +### `BlockEventContext` + +Source: [ScriptBlockEventContext](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockEventContext.java). Inherits `BlockContext`. + +- get `entity` — `Entity getEntity()` + +### `BlockEventHandler` + +Source: [ScriptBlockEventHandler](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockEventHandler.java). + +- method `void handle(BlockEventContext context)` + +### `BlockModule` + +Source: [ScriptBlockModule](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModule.java). Inherits `BlockProperties`. + +- constructor `BlockModule(String name)` +- constructor `BlockModule(String name, String basedOn)` +- constructor `BlockModule(String name, BlockModuleReference basedOn)` +- get `reference` — `BlockModuleReference getReference()` +- method `BlockModule copy(String name)` +- get `name` — `String getName()` +- get `factory` — `String getFactory()` +- set `factory` — `void setFactory(String factory)` +- get `materialDefaults` — `boolean getMaterialDefaults()` +- set `materialDefaults` — `void setMaterialDefaults(boolean materialDefaults)` +- method `void build()` + +### `BlockModuleReference` + +Source: [ScriptBlockModuleReference](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModuleReference.java). + +- constructor `BlockModuleReference(String id)` +- get `id` — `String getId()` + +### `BlockProperties` + +Source: [ScriptBlockProperties](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java). Inherits `DefinitionProperties`. + +- method `BlockProperties addTag(BlockTagReference tag)` +- method `BlockProperties destroyTime(float value)` +- method `BlockProperties explosionResistance(float value)` +- method `BlockProperties strength(float value)` +- method `BlockProperties strength(float hardness, float resistance)` +- method `BlockProperties noCollisions()` +- method `BlockProperties noOcclusion()` +- method `BlockProperties requiresToolToDrop()` +- method `BlockProperties requiresToolToDrop(boolean value)` +- method `BlockProperties noCorrespondingItem()` +- method `BlockProperties noDrops()` +- method `BlockProperties dropsNormally()` +- method `BlockProperties dropsSelf()` +- method `BlockProperties dropsFrom(String table)` +- get `hardness` — `float getHardness()` +- set `hardness` — `void setHardness(float hardness)` +- get `blastResistance` — `float getBlastResistance()` +- set `blastResistance` — `void setBlastResistance(float blastResistance)` +- get `friction` — `float getFriction()` +- set `friction` — `void setFriction(float friction)` +- get `speedFactor` — `float getSpeedFactor()` +- set `speedFactor` — `void setSpeedFactor(float speedFactor)` +- get `jumpFactor` — `float getJumpFactor()` +- set `jumpFactor` — `void setJumpFactor(float jumpFactor)` +- get `lightLevel` — `int getLightLevel()` +- set `lightLevel` — `void setLightLevel(int lightLevel)` +- get `requiresCorrectTool` — `boolean getRequiresCorrectTool()` +- set `requiresCorrectTool` — `void setRequiresCorrectTool(boolean requiresCorrectTool)` +- get `collision` — `boolean getCollision()` +- set `collision` — `void setCollision(boolean collision)` +- get `occlusion` — `boolean getOcclusion()` +- set `occlusion` — `void setOcclusion(boolean occlusion)` +- get `randomTicks` — `boolean getRandomTicks()` +- set `randomTicks` — `void setRandomTicks(boolean randomTicks)` +- get `replaceable` — `boolean getReplaceable()` +- set `replaceable` — `void setReplaceable(boolean replaceable)` +- get `sound` — `String getSound()` +- set `sound` — `void setSound(String sound)` +- get `mapColor` — `String getMapColor()` +- set `mapColor` — `void setMapColor(String mapColor)` +- get `hasItem` — `boolean getHasItem()` +- set `hasItem` — `void setHasItem(boolean hasItem)` +- get `redstoneInput` — `boolean getRedstoneInput()` +- set `redstoneInput` — `void setRedstoneInput(boolean redstoneInput)` +- get `redstoneOutput` — `boolean getRedstoneOutput()` +- set `redstoneOutput` — `void setRedstoneOutput(boolean redstoneOutput)` +- get `strongPower` — `boolean getStrongPower()` +- set `strongPower` — `void setStrongPower(boolean strongPower)` +- get `directional` — `boolean getDirectional()` +- set `directional` — `void setDirectional(boolean directional)` +- get `redstoneDelay` — `int getRedstoneDelay()` +- set `redstoneDelay` — `void setRedstoneDelay(int redstoneDelay)` +- get `comparator` — `String getComparator()` +- set `comparator` — `void setComparator(String comparator)` +- get `outputFaces` — `String[] getOutputFaces()` +- method `BlockProperties addOutputFaces(String... faces)` +- method `BlockProperties removeOutputFaces(String... faces)` +- get `inputFaces` — `String[] getInputFaces()` +- method `BlockProperties addInputFaces(String... faces)` +- method `BlockProperties removeInputFaces(String... faces)` +- method `BlockProperties onRedstoneChanged(RedstoneHandler handler)` +- method `BlockProperties onRightClick(ClickHandler handler)` +- method `BlockProperties onBroken(BreakHandler handler)` +- method `BlockProperties onPlaced(BlockEventHandler handler)` +- method `BlockProperties onSteppedOn(BlockEventHandler handler)` +- method `BlockProperties onRandomTick(BlockEventHandler handler)` + +### `BlockReference` + +Source: [ScriptBlockReference](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockReference.java). + +- constructor `BlockReference(String id)` +- get `id` — `String getId()` + +### `BlockTagReference` + +Source: [ScriptBlockTagReference](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockTagReference.java). + +- constructor `BlockTagReference(String id)` +- get `id` — `String getId()` + +### `BreakContext` + +Source: [ScriptBreakContext](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBreakContext.java). + +- get `player` — `Player getPlayer()` +- get `tool` — `IItemStack getTool()` +- get `block` — `String getBlock()` +- get `x` — `int getX()` +- get `y` — `int getY()` +- get `z` — `int getZ()` +- get `face` — `String getFace()` +- method `void cancel()` +- method `boolean mine(int x, int y, int z)` + +### `BreakHandler` + +Source: [ScriptBreakHandler](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBreakHandler.java). + +- method `void handle(BreakContext context)` + +### `ClickContext` + +Source: [ScriptClickContext](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptClickContext.java). Inherits `BlockContext`. + +- get `player` — `Player getPlayer()` +- get `sneaking` — `boolean isSneaking()` +- get `face` — `String getFace()` + +### `ClickHandler` + +Source: [ScriptClickHandler](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptClickHandler.java). + +- method `void handle(ClickContext context)` + +### `DefinitionProperties` + +Source: [ScriptDefinitionProperties](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java). + +- get `displayName` — `String getDisplayName()` +- set `displayName` — `void setDisplayName(String name)` +- method `void build()` +- get `factoryData` — `IData getFactoryData()` +- set `factoryData` — `void setFactoryData(IData data)` +- get `tags` — `String[] getTags()` +- method `DefinitionProperties addTags(String... tags)` +- method `DefinitionProperties removeTags(String... tags)` +- get `events` — `String[] getEvents()` +- method `DefinitionProperties addEvent(String event, String callback)` +- method `DefinitionProperties removeEvents(String... events)` + +### `HitContext` + +Source: [ScriptHitContext](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptHitContext.java). + +- get `attacker` — `LivingEntity getAttacker()` +- get `target` — `LivingEntity getTarget()` +- get `stack` — `IItemStack getStack()` +- get `damage` — `float getDamage()` + +### `HitHandler` + +Source: [ScriptHitHandler](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptHitHandler.java). + +- method `void handle(HitContext context)` + +### `ItemBuilder` + +Source: [ScriptItemBuilder](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java). Inherits `ItemProperties`. + +- constructor `ItemBuilder(String name)` +- constructor `ItemBuilder(String name, String module)` +- constructor `ItemBuilder(String name, ItemModuleReference module)` +- get `reference` — `ItemReference getReference()` +- get `name` — `String getName()` +- get `module` — `String getModule()` +- method `void build()` + +### `ItemClickContext` + +Source: [ScriptItemClickContext](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemClickContext.java). + +- get `player` — `Player getPlayer()` +- get `client` — `boolean isClient()` +- get `stack` — `IItemStack getStack()` +- get `target` — `String getTarget()` +- get `entity` — `Entity getEntity()` +- get `x` — `int getX()` +- get `y` — `int getY()` +- get `z` — `int getZ()` +- get `face` — `String getFace()` + +### `ItemClickHandler` + +Source: [ScriptItemClickHandler](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemClickHandler.java). + +- method `void handle(ItemClickContext context)` + +### `ItemModule` + +Source: [ScriptItemModule](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java). Inherits `ItemProperties`. + +- constructor `ItemModule(String name)` +- constructor `ItemModule(String name, String basedOn)` +- constructor `ItemModule(String name, ItemModuleReference basedOn)` +- get `reference` — `ItemModuleReference getReference()` +- method `ItemModule copy(String name)` +- get `name` — `String getName()` +- get `factory` — `String getFactory()` +- set `factory` — `void setFactory(String factory)` +- method `void build()` + +### `ItemModuleReference` + +Source: [ScriptItemModuleReference](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModuleReference.java). + +- constructor `ItemModuleReference(String id)` +- get `id` — `String getId()` + +### `ItemProperties` + +Source: [ScriptItemProperties](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java). Inherits `DefinitionProperties`. + +- get `useDuration` — `int getUseDuration()` +- set `useDuration` — `void setUseDuration(int useDuration)` +- get `useAnimation` — `String getUseAnimation()` +- set `useAnimation` — `void setUseAnimation(String useAnimation)` +- method `ItemProperties onUseStart(ItemUseHandler handler)` +- method `ItemProperties onUseTick(ItemUseHandler handler)` +- method `ItemProperties onUseStop(ItemUseHandler handler)` +- method `ItemProperties onUseFinish(ItemUseHandler handler)` +- method `ItemProperties onHit(HitHandler handler)` +- method `ItemProperties onBreakBlock(BreakHandler handler)` +- method `ItemProperties onRightClick(ItemClickHandler handler)` +- get `stackSize` — `int getStackSize()` +- set `stackSize` — `void setStackSize(int stackSize)` +- get `durability` — `int getDurability()` +- set `durability` — `void setDurability(int durability)` +- get `fireResistant` — `boolean getFireResistant()` +- set `fireResistant` — `void setFireResistant(boolean fireResistant)` +- get `container` — `String getContainer()` +- set `container` — `void setContainer(String container)` +- method `ItemProperties configureTool(ToolSettings settings)` +- method `ItemProperties remainder(ItemReference item)` +- method `ItemProperties stacksTo(int size)` +- method `ItemProperties craftRemainder(String item)` +- method `ItemProperties craftRemainder(ItemReference item)` + +### `ItemReference` + +Source: [ScriptItemReference](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemReference.java). + +- constructor `ItemReference(String id)` +- get `id` — `String getId()` + +### `ItemUseContext` + +Source: [ScriptItemUseContext](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemUseContext.java). + +- get `phase` — `String getPhase()` +- get `entity` — `LivingEntity getEntity()` +- get `client` — `boolean isClient()` +- get `stack` — `IItemStack getStack()` +- get `duration` — `int getDuration()` +- set `duration` — `void setDuration(int duration)` +- get `result` — `IItemStack getResult()` +- set `result` — `void setResult(IItemStack result)` +- method `void cancel()` + +### `ItemUseHandler` + +Source: [ScriptItemUseHandler](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemUseHandler.java). + +- method `void handle(ItemUseContext context)` + +### `MaterialBuilder` + +Source: [ScriptMaterialBuilder](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialBuilder.java). + +- constructor `MaterialBuilder(String name)` +- get `name` — `String getName()` +- get `type` — `String getType()` +- set `type` — `void setType(String type)` +- get `color` — `int getColor()` +- set `color` — `void setColor(int color)` +- get `hardness` — `double getHardness()` +- set `hardness` — `void setHardness(double hardness)` +- get `strength` — `double getStrength()` +- set `strength` — `void setStrength(double strength)` +- get `magic` — `double getMagic()` +- set `magic` — `void setMagic(double magic)` +- get `damage` — `double getDamage()` +- set `damage` — `void setDamage(double damage)` +- get `hasFluid` — `boolean getHasFluid()` +- set `hasFluid` — `void setHasFluid(boolean hasFluid)` +- get `fluid` — `String getFluid()` +- set `fluid` — `void setFluid(String fluid)` +- get `items` — `String[] getItems()` +- method `MaterialBuilder addItems(String... names)` +- method `MaterialBuilder removeItems(String... names)` +- get `blocks` — `String[] getBlocks()` +- method `MaterialBuilder addBlocks(String... names)` +- method `MaterialBuilder removeBlocks(String... names)` +- get `harvestTags` — `String[] getHarvestTags()` +- method `MaterialBuilder addHarvestTags(String... tags)` +- method `MaterialBuilder removeHarvestTags(String... tags)` +- method `MaterialBuilder addSpecial(String key, double value)` +- method `MaterialBuilder removeSpecial(String... keys)` +- method `void build()` +- get `repairItem` — `String getRepairItem()` +- set `repairItem` — `void setRepairItem(String value)` +- get `incorrectBlocks` — `String getIncorrectBlocks()` +- set `incorrectBlocks` — `void setIncorrectBlocks(String value)` +- get `baseBlock` — `String getBaseBlock()` +- set `baseBlock` — `void setBaseBlock(String value)` +- get `blockSetType` — `String getBlockSetType()` +- set `blockSetType` — `void setBlockSetType(String value)` +- get `blockSound` — `String getBlockSound()` +- set `blockSound` — `void setBlockSound(String value)` +- get `mapColor` — `String getMapColor()` +- set `mapColor` — `void setMapColor(String value)` +- method `MaterialBuilder toolDurability(int value)` +- method `MaterialBuilder toolEfficiency(double value)` +- method `MaterialBuilder attackDamage(double value)` +- method `MaterialBuilder enchantability(int value)` +- method `MaterialBuilder repairWith(ItemReference item)` +- method `MaterialBuilder baseBlockFrom(BlockReference block)` +- method `MaterialBuilder incorrectBlocksTag(BlockTagReference tag)` +- method `MaterialBuilder addHarvestTag(BlockTagReference tag)` +- method `MaterialBuilder addItemModule(ItemModuleReference module)` +- method `MaterialBuilder addBlockModule(BlockModuleReference module)` +- get `displayName` — `String getDisplayName()` +- set `displayName` — `void setDisplayName(String value)` + +### `RedstoneContext` + +Source: [ScriptRedstoneContext](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptRedstoneContext.java). Inherits `BlockContext`. + +- get `power` — `int getPower()` +- get `previous` — `int getPrevious()` +- get `powered` — `boolean isPowered()` + +### `RedstoneHandler` + +Source: [ScriptRedstoneHandler](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptRedstoneHandler.java). + +- method `void handle(RedstoneContext context)` + +### `ToolSettings` + +Source: [ScriptToolSettings](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptToolSettings.java). + +- constructor `ToolSettings()` +- set `mineable` — `void setMineable(String tag)` +- set `fastOn` — `void setFastOn(String tag)` +- set `attackDamage` — `void setAttackDamage(float value)` +- set `attackSpeed` — `void setAttackSpeed(float value)` +- method `ToolSettings abilities(String... names)` +- method `ToolSettings durability(double factor, int minimum, String stat)` +- method `ToolSettings speed(double factor, double minimum, String stat)` +- method `ToolSettings mineableTag(BlockTagReference tag)` +- method `ToolSettings fastOnTag(BlockTagReference tag)` + +## Definition-to-runtime map + +Paths below are relative to `src/main/java/zone/moddev/mc/lib/`. + +| Exposed surface | Definition/planning path | Runtime or resource consumer | +| --- | --- | --- | +| Material identity, type, color, hardness, strength, magic, damage, special stats | `ScriptMaterialBuilder.build` -> `MaterialProperties` / `MaterialFactory` | `common/runtime/MaterialStats.java`, `MaterialToolTier.java`, standard item/block factories; color also feeds script texture generation. Type is classification metadata, not a new Java implementation. Arbitrary special keys only affect behavior if a factory reads them. | +| Material item/block selection, fluid selection, harvest tags | `MaterialSelection` -> `MaterialFactory` | `common/runtime/RuntimeContentRegistrar.java` queues selected content; `ScriptMaterialResources.data` emits tags. Projectile selection follows its item. | +| Standalone block/item identity and selected module | `ScriptBlockBuilder` / `ScriptItemBuilder` -> `StandaloneBlock` / `StandaloneItem` | Same definition planning and runtime registrar as file definitions. Standalone items use default material runtime settings, not configurable independent tiers. | +| Module factory, block materialDefaults, copied module settings | `ScriptBlockModule` / `ScriptItemModule` -> `DataModules` and public module registration | `MaterialFactory` plans selected parts; `StandardImplementations` or an addon implementation creates objects. `materialDefaults` controls material-stat application to block templates. | +| Common block properties including sound/mapColor | `BlockTemplate` -> `MMDBlock.Properties` | `common/runtime/RuntimeProperties.block`; individual factory choices can further specialize vanilla properties. | +| Block baseBlock / blockSetType | Standalone `MaterialRuntimeSettings` | `RuntimeContentRegistrar` block context and the stairs/door/trapdoor/button/plate factories that use these values. These are not universal behavior overrides. | +| hasItem | `BlockTemplate` -> `MMDBlock` | Registrar queues a block item only when present in the planned block definition; resources follow the plan. | +| blockEntityData | Standalone block factory/block-entity configuration | Existing block-entity planning/creation path; supplying data does not create a new machine implementation or script-defined screen. | +| Item stackSize, durability, fireResistant, container | `ItemTemplate` -> `MMDItem` | `RuntimeProperties.item`; standard factories can derive/override durability from the material tier. | +| Item useDuration / useAnimation | `ItemTemplate.DelayedUse` -> `MMDItem.useTime/useAnim` | `StandardImplementations` basic_item factory passes values to `StandardItems.Fuel` / `Basic`, which implement timed use. Other factories keep their native use behavior; these fields do not override bows or make every item usable. | +| factoryData | Module/standalone `FactoryData` | Selected Java factory's decoder. Unknown or unsupported settings do not automatically gain runtime behavior. | +| addTags/removeTags | Block template tags or script item-tag collection | `ScriptMaterialResources.data` emits generated tag entries. | +| addEvent/removeEvents and onXxx functions | `ScriptDefinitionProperties` registers callback IDs and event bindings | `common/runtime/ItemEventBridge.java`, `BlockHooks.java`, `RedstoneBlock.java`, and `api/MaterialEventContexts.java`; see callback table below. | +| Redstone properties and input/output faces | Existing `factory_data.redstone` schema | `RedstoneBlock.Settings` validates; `RedstoneBlock` implements signals, scheduling, facing, and comparator behavior. Explicit redstone configuration requires factory `mmdlib:block`. | +| BlockContext.input/output/facing and coordinates | Wrappers over material event contexts | Reads current block state/signals; output changes are server-only, clamped to 0–15, and require an output-capable RedstoneBlock. | +| Generated models/names/textures/loot | Planned script content plus module template ancestry | `ScriptMaterialResources` -> `MaterialPacks`; templates and `scripts/mmdlibdata` supply resources. No recipes are generated by this integration. | + +## Callback contracts and context limits + +Every handler interface exposes `handle(context)` as listed above. Script functions +are retained as callbacks; they are not rerun to rebuild content during gameplay. + +| Builder callbacks | Contract / runtime dispatcher | Side and writable results | +| --- | --- | --- | +| Item onRightClick | tool-right-click / ItemEventBridge | Both sides; observational callback alongside native use, no script cancel/result setter. Target-specific fields must be checked against target. | +| Item onUseStart/Tick/Stop/Finish | item-use-start/tick/stop/finish / ItemEventBridge | Both sides; duration writable at start, cancel at start/tick, result at finish. Requires an item with actual timed-use behavior. | +| Item onHit | item-hit / ItemEventBridge | Server, after melee damage; damage is read-only. | +| Item onBreakBlock, block onBroken | item-break-block then block-broken / ItemEventBridge | Server before player break; cancel supported. BreakContext.mine uses guarded player destruction, protection checks, harvest eligibility, drops, experience and tool wear. | +| Block onPlaced | block-placed / ItemEventBridge | Server; entity may be absent. | +| Block onSteppedOn | block-stepped-on / BlockHooks | Both sides; needs mmdlib:block. | +| Block onRandomTick | block-random-tick / BlockHooks | Server; enables randomTicks and needs mmdlib:block. | +| Block onRightClick | block-right-click / ItemEventBridge | Both sides; consumes click. Output mutation only has an effect on a compatible redstone block. | +| Block onRedstoneChanged | redstone-changed / RedstoneBlock | Server; input is enabled automatically; configured delay and face selection apply. | + +World mutation in callbacks that run on both sides must be guarded with `client`. +BreakContext and HitContext are server-only and do not expose a client property. +BlockEventContext.entity and ItemClickContext.entity can be null. ItemClickContext +coordinates are zero and face is empty for targets other than a block. Exceptions +from script callback execution are logged by the binding wrapper. + +## Capability classification for parity work + +| Status | Capability | Evidence / consequence | +| --- | --- | --- | +| Script-exposed and runtime-supported | Common block/item properties, selected shapes, materials, modules, callbacks, redstone, resource generation | Consumer map above. Factory-specific limits still apply. | +| Script-exposed with limited consumers | useDuration/useAnimation, arbitrary factoryData and special stats | Only the documented consuming factory gives these values behavior. | +| Script-exposed in phase 2 | Material repairItem, incorrectBlocks, baseBlock, blockSetType, blockSound, mapColor | `MaterialRuntimeSettings` and registrar/factory context consumers; MaterialBuilder now exposes these settings while preserving defaults. | +| Runtime-supported through an existing generic script map | Tool stat overrides and generic tool settings; basic-item burn time | `MaterialStats`, `StandardItems.ToolSettings`, `StandardImplementations`; addSpecial/factoryData already reach them. Named stat methods and ToolSettings now wrap these existing paths; per-tool durability scaling is limited by TieredItem overriding durability. | +| Schema-only in current standard runtime | food reference, lore, declared toolActions, general attributeModifiers, delayed-use onComplete/useFinishMode, creativeStacks | Stored in ItemTemplate/MMDItem; no standard runtime consumer found. Native tool attributes and generic tool abilities are separate implemented paths. | +| Unavailable as a configurable definition capability | Item rarity/feature flags; advanced block predicates, piston reaction, instrument, offsets, solidity flags | Current definition/runtime property path does not represent and apply them. | +| Unavailable as a public script registration path | Custom sound events/types, independent tool tiers, user-defined creative tabs | Existing material tab/armor/tier machinery is not a general script builder API. | +| Script-exposed in phase 3 | Typed ID references and module snapshot copies | New wrappers unwrap into existing consumers; copy rebinds locally owned callbacks with separate handler maps. | +| Script-exposed in phase 4 | Fluent builder aliases, displayName, default/self/empty/referenced loot | Aliases use existing setters. Immutable ScriptResourceOptions metadata feeds the generated pack; external resources keep priority. | +| Unavailable script conveniences | Arbitrary game-object property cloning | Module configuration copying does not clone registered game objects. | + +## Startup and copying rules + +`MMDLibCraftTweakerPlugin` registers loader `mmdlib`, inheriting `crafttweaker` types. +`CraftTweakerInit` installs a definition source; `ScriptDefinitions.load` executes it +before registry events. Module build registers a catalog module immediately so later +script statements and material files can use it. Material and standalone build queue +content for normal planning. Building outside the startup run fails. A startup script +error is logged and definitions built before that failure can remain; this is not a +transactional all-or-nothing script loader. + +New content IDs are restricted to `mmdlib:`. Selecting an existing module/factory +from another namespace is distinct from creating content in that namespace. Scripts +and client resources must be distributed to both sides. `/reload` does not rerun +content definitions; changing them requires a restart. Registry-dependent recipe +operations belong in ordinary CraftTweaker scripts. + +Constructing a module from another module copies its configuration; it does not +clone a registered vanilla/modded object, track future edits to its source, or create +a Java subclass. Existing event bindings may refer to shared callback IDs: copying +bindings is not a deep copy of executable closures. Material selection of a module +uses its template and factory under the existing material-default rules. + +## Next-phase gates + +Each proposed addition in [the parity plan](crafttweaker-parity-plan.md) must target +an existing consumer in this audit. Phase 2 exposes runtime settings; phase 3 now wraps IDs +and copies supported module configurations. Phase 4 resource controls operate through +ScriptMaterialResources/MaterialPacks. A schema-only field needs a runtime change +and remains deferred under this plan. New registry types and event contracts also +remain deferred. This audit establishes source support only; it does not certify +new script compilation, server execution, client rendering, or packaging results. + +## Phase 2 verification + +`./gradlew --offline check` passed with 185 unit tests, 83 MMDLib server GameTests, +52 companion GameTests, release/companion packaging checks, and static-asset +validation. The new script fixture is `src/runtimeTest/scripts/mmdlib_tools.zs`; +`ScriptHookSmoke.toolSettings` checks the registered material tool and runtime +settings. No client rendering or launch verification was performed for phase 2. + +The GameTest also exposed an existing runtime limit: vanilla TieredItem calls +`properties.durability(tier.getUses())`, replacing the generic tool factory's scaled +durability. The test records actual material-tier durability; codec/unit coverage +only proves that helper scaling configuration is represented correctly. Speed +scaling has a separate runtime assertion. No core fix is included. + +## Phase 3 verification + +`./gradlew --offline check` passed with 187 unit tests, 84 MMDLib server GameTests, +52 companion GameTests, packaging checks, and static-asset validation. The new +`mmdlib_references.zs` fixture verifies item/block module constructors using typed +references, independently edited property/tag snapshots, separately rebound item +and block callbacks, same-run tag inheritance, and a forward item remainder +reference resolved by the existing registrar. Unit tests check unregistered IDs +without registry lookup and malformed ID rejection. Wrong-kind rejection follows +from the distinct exposed parameter types; no runtime registry-kind probing was +added. No client smoke test was performed in this phase. + +## Phase 4 verification + +The phase 4 tests cover fluent/property definition equivalence and matching +validation failures, generated material/module name expansion, empty and nested +loot JSON, and rejected direct self-reference/self-drop-without-item requests. +`ScriptHookSmoke.generatedLoot` performs actual server player block breaks for +normal/self/no-drop/referenced tables, itemless blocks, and a folder-pack override +that replaces generated noDrops with a diamond drop. Existing resource tests retain +shape-aware double-slab loot coverage. English-name resource content is unit-tested; +client rendering and localization display have not been smoke-tested. + +Final `./gradlew --offline check` passed: 190 unit tests, 85 MMDLib GameTests, +52 companion GameTests, packaging and static-asset validation. + +## Phase 5 final verification + +Normal `./gradlew --offline check build` passed (190 unit tests, 85 MMDLib and +52 companion GameTests, packaging and static assets). Real integrated-client +launches passed with CraftTweaker installed and absent. Generated English names, +models, textures, and four horizontal front-face rotations passed assertions; +world and item-gallery captures were visually inspected. Release JAR inspection +confirmed no smoke content remained after restoring normal main outputs. +See [the verification report](crafttweaker-verification.md). The existing core +per-tool durability-scaling limitation remains deferred. diff --git a/docs/crafttweaker-parity-plan.md b/docs/crafttweaker-parity-plan.md new file mode 100644 index 00000000..4c5e9998 --- /dev/null +++ b/docs/crafttweaker-parity-plan.md @@ -0,0 +1,183 @@ +# CraftTweaker integration parity plan + +Status: phases 1–5 complete within the integration-only scope. The phase 2 +per-tool durability-scaling acceptance item remains deferred pending a core fix. +See [the API inventory and runtime audit](crafttweaker-api-inventory.md). + +Baseline: MMDLib `1862428`; ContentTweaker `1.20.1` at +[`049253e587d92a99f49e31c10ca2c9c20c967eb6`](https://github.com/CraftTweaker/ContentTweaker/tree/049253e587d92a99f49e31c10ca2c9c20c967eb6). + +## Scope and acceptance boundary + +Bring the scripting integration closer to ContentTweaker using functionality that +MMDLib already implements. Preserve current script syntax and registry identities. +No core schema, registrar, event-contract, or standard-runtime changes are planned. +Implementation belongs in `compat/CraftTweaker`, its generated resources, tests, +and documentation. A feature that needs changes elsewhere is deferred and recorded +with the missing runtime capability rather than silently widening this scope. + +Use the public definition-source, module, implementation, and event APIs. Do not +replace standard factories, register parallel script-only versions of standard +items/blocks, or inject components after registration to simulate missing support. +Do not claim parity merely because a value can be stored in a definition. + +## Capability decisions + +| ContentTweaker capability | Proposed MMDLib treatment | Boundary | +| --- | --- | --- | +| Common block/item settings | Add familiar fluent aliases over existing properties | Integration only; preserve existing validation | +| Deferred object references | Add typed ID wrappers accepted by new overloads | Store IDs; resolve only where existing runtime already resolves them | +| Clone properties | Copy supported MMDLib module/builder configuration | No arbitrary vanilla/modded object cloning promise | +| Tool configuration | Expose material runtime settings and typed helpers for existing tool factory data | Material-derived tier; no new tier registry | +| Material/block/item families | Retain current material and module APIs | Already broader than this ContentTweaker branch | +| Gameplay callbacks | Retain current hooks and improve discovery/examples | No additional core event contracts | +| Generated resources and drops | Expose names and simple loot options through the existing generated pack | Generated resources only; no change to block runtime loot-source properties | +| Food, rarity, arbitrary attributes | Defer | RuntimeProperties does not apply these as configurable definition properties | +| Creative tabs and sound registration | Defer | No existing matching MMDLib public definition/registration path | +| Advanced block predicates/properties | Defer | Current block schema and runtime do not represent/apply them | + +## Phase 1: establish the supported surface + +- [x] Inventory every exposed constructor, property, method, callback context, and + inherited member. Map each to its actual definition and runtime consumer. +- [x] Correct `crafttweaker.md`: use duration and animation are implemented; food, + lore, attributes, and tool actions must be described individually by runtime support. +- [x] Document module inheritance versus copying properties from a game object, + startup-only definitions, namespace restrictions, side rules, and restart needs. +- [x] Publish a capability table distinguishing script-exposed, runtime-supported + but unexposed, schema-only, and unavailable features. + +Acceptance: every proposed addition has a named existing runtime consumer or an +existing generated-resource consumer. Unsupported fields remain unexposed. + +## Phase 2: expose material runtime and tool settings + +- [x] Add MaterialBuilder accessors for existing MaterialRuntimeSettings fields: + repair item, incorrect-block tag, base block, block-set type, block sound, and map + color. Preserve current generated texture/armor defaults. +- [x] Add named helpers for existing material special stats: tool durability, + efficiency, attack damage, and enchantability, after checking MaterialStats + precedence and validation. Keep `addSpecial` available. +- [x] Add a typed configuration helper for the existing `mmdlib:tool` factory: + mineable/fast-on tags, abilities, attack damage/speed, durability and speed scaling. + Serialize to the existing factory_data keys and use its existing decoder. +- [x] Document material-wide settings versus per-module settings and which factories + actually consume each value. Do not imply the generic tool helper configures all + vanilla-shaped tool factories. + +Acceptance: a script-defined material tool demonstrates repair, harvest restrictions, +stats, and configured abilities in the server runtime. No synthetic independent tier +or numeric harvest-level mapping is introduced. + +Runtime limitation discovered during phase 2: `StandardItems.Tool` computes scaled +durability before invoking vanilla `TieredItem`, whose constructor calls +`properties.durability(tier.getUses())`. Thus material durability works, but the +helper's durability scaling does not affect the final item. Core changes remain +out of scope; this part of acceptance stays open: + +- [ ] Verify effective per-tool durability scaling after a separately authorized + core fix. Do not advertise it as implemented runtime behavior. + +Verification: `./gradlew --offline check` passed (185 unit tests, 83 MMDLib +GameTests, 52 companion GameTests, packaging and static assets). No client smoke +check was run for this phase. + +## Phase 3: references and configuration copying + +- [x] Add typed wrappers for item, block, module, and block-tag IDs where consumed + by current builders. Keep string forms for compatibility. +- [x] Add overloads that unwrap references into existing definition IDs. References + must not expose eager registry getters in the startup loader. +- [x] Add explicit configuration-copy operations for supported MMDLib modules and, + where useful, unbuilt builders. Snapshot collections/maps; define overrides as + source configuration first, caller edits second. +- [x] Exclude registry identity, aliases, and callback ownership from blind copying. + Specify how callback bindings are reused without sharing mutable handler maps. +- [x] Reject unknown source modules and wrong reference kinds with script-local + errors. Leave cross-definition runtime references to the existing resolution path. + +Acceptance: copied configurations can be edited independently; forward ID references +work wherever the existing runtime supports them; invalid references fail without +creating partial content. Arbitrary registry-object cloning remains documented as +unsupported. + +Implementation notes: explicit `ItemModule.copy` and `BlockModule.copy` snapshot +unbuilt or built module builders. Standalone builder cloning and arbitrary game +object cloning are not added. Locally owned callbacks get new IDs and independent +handler maps; captured variables and external callback IDs remain shared. Typed +references do not add registry getters or relax existing resolution order. Same-run +item-module tag lookup now reads the active script collection. + +Phase 3 verification: `./gradlew --offline check` passed with 187 unit tests, +84 MMDLib GameTests, 52 companion GameTests, packaging and static assets. No client +smoke test was run. The previous core durability limitation remains deferred. + +## Phase 4: familiar builder conveniences and resources + +- [x] Add selected fluent aliases: `stacksTo`, `craftRemainder`, `destroyTime`, + `explosionResistance`, `strength`, `noCollisions`, `noOcclusion`, + `requiresToolToDrop`, and `noCorrespondingItem`. Delegate to existing setters. + Use collision-free names when properties already occupy a scripting member name. +- [x] Keep existing constructors and `build()` behavior; do not change return types + or adopt ContentTweaker syntax wholesale. +- [x] Add display-name overrides and simple generated loot choices: default/self, + no drops, or an existing loot-table reference, subject to vanilla 1.21.1 loot + semantics and the existing generated pack's resource priority. +- [x] Document external resource overrides for complex loot/models and retain the + current material texture generation and directional block templates. + +Acceptance: generated resource contents and override precedence are checked; a +server break test verifies drop behavior. Aliases and property syntax produce the +same definitions and the same validation errors. + +Phase 4 verification: `./gradlew --offline check` passed with 190 unit tests, +85 MMDLib GameTests, 52 companion GameTests, packaging checks, and static assets. +The server test proves external loot overrides take priority. No client smoke test +was performed; the phase 2 core durability limitation remains deferred. + +## Phase 5: verify and document the supported parity + +- [x] Add focused unit coverage for reference kinds, snapshots, override precedence, + aliases, validation, and generated resources. +- [x] Extend the existing CraftTweaker GameTest scripts with representative material, + tool, copy, and loot scenarios. Run `./gradlew check` and package verification. +- [x] Check a launch without CraftTweaker and a real client launch with it installed. + Verify names/models/textures and directional rendering separately from server tests. +- [x] Update examples, API documentation, and changelog. Record passed checks and + unresolved client/runtime checks separately; leave unverified tasks unchecked. + +Final phase 5 evidence: normal `./gradlew --offline check build` passed with +190 unit tests, 85 MMDLib GameTests, 52 companion GameTests, packaging and static +assets. Isolated real-client launches passed with CraftTweaker present and absent. +Generated names/textures/models and four horizontal front-face rotations were +asserted, and both rendered captures were inspected. The release JAR contains the +integration classes and excludes smoke classes/scripts/resources. See the +[verification report](crafttweaker-verification.md) for commands, captures and limits. + +## Deferred features and reasons + +- Food creation: food fields exist in the definition, but RuntimeProperties.item + currently applies only stack size, durability, fire resistance, and remainder. +- Rarity, feature flags, general attribute configuration, lore, and new item actions: + lack a complete applicable runtime path for this integration scope. +- Full CreativeTabBuilder, SoundEventBuilder, and custom SoundTypeFactory: would + require a new registration capability or a parallel integration-owned system. +- State-dependent light/map color, spawning/suffocation/view/emissive predicates, + piston reaction, instrument, offsets, and solidity flags: need new core property + representation/application. `factoryData` alone cannot make these work. +- Independent tool-tier factories and arbitrary vanilla/modded object cloning: + cannot be represented faithfully through current material/module settings. +- Inventory tick, tooltip, crafting, pickup/toss, entity-inside, and fall hooks: + require additional event contracts or runtime overrides. + +## Source anchors + +- [Current scripting API](crafttweaker.md) +- [Runtime property application](../src/main/java/zone/moddev/mc/lib/common/runtime/RuntimeProperties.java) +- [Material runtime settings](../src/main/java/zone/moddev/mc/lib/common/types/MaterialRuntimeSettings.java) +- [Material stats](../src/main/java/zone/moddev/mc/lib/common/runtime/MaterialStats.java) +- [Standard factories](../src/main/java/zone/moddev/mc/lib/common/runtime/StandardImplementations.java) +- [Public extension API](../src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java) +- [Script resource generation](../src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java) +- [ContentTweaker builders](https://github.com/CraftTweaker/ContentTweaker/tree/049253e/vanilla/src/api/java/com/blamejared/contenttweaker/vanilla/api/zen/builder) +- [ContentTweaker references](https://github.com/CraftTweaker/ContentTweaker/blob/049253e/core/src/api/java/com/blamejared/contenttweaker/core/api/zen/object/Reference.java) diff --git a/docs/crafttweaker-verification.md b/docs/crafttweaker-verification.md new file mode 100644 index 00000000..c5ce613c --- /dev/null +++ b/docs/crafttweaker-verification.md @@ -0,0 +1,76 @@ +# CraftTweaker integration verification + +Verification of phases 1–5 on the current working tree, 7 November 2026. +No independent core feature extensions are included. The previously identified +TieredItem durability-scaling limitation remains deferred. + +## Reproduce + +```sh +LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe xvfb-run -a ./gradlew --offline runClient -I scripts/crafttweaker-smoke.init.gradle +LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe xvfb-run -a ./gradlew --offline runClient -I scripts/crafttweaker-smoke.init.gradle -PwithoutCraftTweaker +./gradlew --offline check build +``` + +The smoke initializer uses separate build/ct-smoke-present and +build/ct-smoke-absent directories; it does not touch a normal development world or +scripts folder. It injects only src/clientSmoke/java for the development launch. +Always run normal verification afterward to restore main output and prove test +classes are absent from the release JAR. The test task rejects a missing pass +marker or any MMDLIB_CT_CLIENT_FAIL marker. + +Software rendering was needed here: the native WSL graphics path crashed in the +system loader before mod loading. The retry used llvmpipe/Mesa under Xvfb; these +results do not validate a particular hardware GPU driver. + +## Client evidence + +The installed-mod run passed its assertions and opened an integrated world: + +- Startup script content registered on client and integrated server. +- Material English label and standalone display-name override matched. +- Five representative item models and their particle sprites resolved. +- Each horizontal gate facing selected the front texture on the matching face. +- Actual rendered captures were inspected: colored material ingot/pickaxe/block, + named widget, gate item, and four rotated gate blocks with distinct side/front/top. + +Local captures (generated build artifacts, not release resources): + +- [Item and name gallery](../build/ct-smoke-present/screenshots/ct-items-and-names.png) +- [Directional world scene](../build/ct-smoke-present/screenshots/ct-directional-world.png) + +Client logs are under each smoke directory's logs/latest.log. Inspect +MMDLIB_CT_CLIENT_PASS and the actual mod list when reproducing. The absent-mod run +must report CraftTweaker installed=false and exclude script-only content. + +## Automated coverage + +The earlier full phase 4 check passed 190 unit tests, 85 MMDLib GameTests and +52 companion GameTests, plus release/companion packaging and static assets. +Coverage includes typed IDs and malformed references, module configuration/tag and +callback snapshots, same-run tag inheritance, forward item remainder resolution, +material/tool settings, fluent/property equivalence and validation, generated names +and loot, actual player block breaks, and folder-pack loot override precedence. + +Executable examples: + +- [Material and tool settings](../src/runtimeTest/scripts/mmdlib_tools.zs) +- [Typed references and copies](../src/runtimeTest/scripts/mmdlib_references.zs) +- [Fluent builders and loot](../src/runtimeTest/scripts/mmdlib_resources.zs) +- [Real-client resource fixture](../src/clientSmoke/scripts/content.zs) + +The client fixture is representative rather than an exhaustive visual check of +every material part. English names are tested; other locales are authored through +normal resource overrides. Captured variables inside copied script functions remain +shared, while callback IDs/maps are independent. Reference wrappers preserve the +existing runtime's dependency-resolution restrictions. + +## Final result + +Both client launches passed: installed=true and installed=false. The present-mod +captures linked above were visually inspected. Normal `./gradlew --offline check +build` then passed: 190 unit tests, 85 MMDLib GameTests, 52 companion GameTests, +static assets and packaging. Inspection of +`build/libs/mmdlib-2.0.0.121012-development.jar` confirmed that the integration +classes are included and smoke classes, fixture scripts and fixture resource IDs +are absent. No commit or remote publication was performed. diff --git a/docs/crafttweaker.md b/docs/crafttweaker.md index 2c859059..05ac3dc0 100644 --- a/docs/crafttweaker.md +++ b/docs/crafttweaker.md @@ -1,7 +1,7 @@ # CraftTweaker definitions -When CraftTweaker is installed, scripts can define MMDLib materials, blocks, and block -modules. Scripts use the `mmdlib` loader and run once at startup, after every mod has +When CraftTweaker is installed, scripts can define MMDLib materials, blocks, items, +and reusable block and item modules. Scripts use the `mmdlib` loader and run once at startup, after every mod has constructed and before registry events. What they build is planned, checked, and registered like the matching `mmdlib/` files. Changing a script needs a restart; `/reload` does not run these scripts again. @@ -11,6 +11,10 @@ The `mmdlib` loader inherits CraftTweaker's `crafttweaker` loader, so its types world, such as `` brackets or recipe managers, cannot run this early; use a normal `crafttweaker` script for recipes. +See the [complete API inventory and runtime capability audit](crafttweaker-api-inventory.md) +for inherited members, factory-specific limits, and the boundary between stored +definition fields and implemented behavior. + ## Materials ```zenscript @@ -51,6 +55,33 @@ or another script fails loading. See [the material factory](material-factory.md) for what each stat drives. +### Material runtime and tool settings + +MaterialBuilder also exposes these string properties. IDs are stored until normal +runtime registration; sounds and map colors name vanilla constant keys. + +| Property | Default | Meaning | +| --- | --- | --- | +| `repairItem` | `""` | explicit repair item ID; empty restores the material's normal repair choice | +| `incorrectBlocks` | `minecraft:incorrect_for_iron_tool` | block tag this material's tools cannot harvest | +| `baseBlock` | `minecraft:iron_block` | base behavior for factories such as stairs | +| `blockSetType` | `minecraft:iron` | behavior for doors, buttons and pressure plates | +| `blockSound` | `""` | material block sound override, e.g. `wood`; empty keeps module sound | +| `mapColor` | `""` | material map-color override, e.g. `gold`; empty keeps module map color | + +Named methods `toolDurability(int)`, `toolEfficiency(double)`, +`attackDamage(double)`, and `enchantability(int)` write the existing special keys +`tool_durability`, `tool_efficiency`, `base_attack_damage`, and `enchantability`. +They return the builder for chaining. Durability must be at least 1; efficiency +and enchantability must be nonnegative. Damage may be negative; decimal values +must be finite and fit a float. `addSpecial`/`removeSpecial` still work; the last +write to a key wins, and removing it restores the derived stat. + +These settings apply to the material's parts. They do not expose a separate tier +registry or override every item factory's native behavior. Existing special stats +can take precedence over derived values; a tool module can additionally scale +material durability and efficiency. + ### Collections Each collection has an add and a remove function. They take any number of names, @@ -193,7 +224,7 @@ longbow.build(); | `stackSize` | int | 1 to 99 | | `durability` | int | uses before breaking; 0 for none. Setting it also sets `stackSize` to 1 | | `fireResistant` | bool | survives fire and lava | -| `useDuration` | int | ticks of use when right-clicked, like eating; 0 (default) for none. See [Hooks](#using-an-item-over-time) | +| `useDuration` | int | ticks of use for the basic-item factory; 0 (default) for none. Other factories keep their native behavior. See [Hooks](#using-an-item-over-time) | | `useAnimation` | string | how it is held while used: `none`, `eat`, `drink`, `bow`... | | `container` | item ID | left in the crafting grid after use, such as `minecraft:bucket`; `""` for nothing | | `factoryData` | map | settings for the module's Java implementation | @@ -231,6 +262,192 @@ tin.build(); A module's tags go to every material part built from it. The part's art comes from the template of the module it was copied from, in the material color. +## Fluent conveniences and generated resource controls + +Property assignment remains supported. The following fluent aliases return their +properties builder and delegate to the same setters, so validation timing and +errors remain the same. `build()` still returns void. + +| Builder/module | Fluent methods | Existing properties | +| --- | --- | --- | +| Item | `stacksTo(int)`, `craftRemainder(string or ItemReference)` | stackSize, container | +| Block | `destroyTime(float)`, `explosionResistance(float)` | hardness, blastResistance | +| Block | `strength(float)` or `strength(hardness, resistance)` | hardness and blastResistance | +| Block | `noCollisions()`, `noOcclusion()` | collision=false, occlusion=false | +| Block | `requiresToolToDrop()` or `requiresToolToDrop(bool)` | requiresCorrectTool | +| Block | `noCorrespondingItem()` | hasItem=false | + +Chains return the shared property type. Use a variable when you need module-only +members such as `factory` or `copy` after a chain. + +MaterialBuilder, ItemBuilder, BlockBuilder, ItemModule and BlockModule expose +`displayName`. It overrides the English name generated in `en_us.json`; it does +not change registry IDs, model names, texture paths, or other locales. Blank names +are rejected. A material name supplies the base label for its generated parts; +a module name overrides its selected parts' labels. The token `__material__` in +a module label expands to that material's display name. Standalone builders inherit +module resource settings at construction and can override them. Module `copy` +snapshots these settings as well. + +```zenscript +var block = new BlockBuilder("polished_stone"); +block.displayName = "Polished Workshop Stone"; +block.strength(2, 6).requiresToolToDrop().dropsSelf(); +block.build(); +``` + +Blocks and block modules expose these generated-loot controls. The last call wins: + +| Method | Generated block loot | +| --- | --- | +| `dropsNormally()` | existing shape-aware rules: double slabs/slabs, doors and anvils retain their special handling | +| `noDrops()` | an explicit empty table | +| `dropsSelf()` | one corresponding block item, subject to explosion survival; requires hasItem=true | +| `dropsFrom("namespace:path")` | a nested reference to an existing loot table, using the block's loot context | + +`dropsSelf` intentionally bypasses shape-specific rules. Prefer `dropsNormally` +for slabs and doors. `noCorrespondingItem` suppresses the ordinary self-drop table; +the existing double-slab-to-slab default remains intact. A referenced table must +accept block-context parameters and must not recurse through this block's table. +Direct self-reference is rejected; indirect cycles and missing external tables +remain subject to vanilla loot validation. `dropsFrom` creates a generated wrapper; +it does not change the core block's loot-table ID. + +These controls live entirely in the generated pack. Complex loot, custom models, +and translations can be supplied in `scripts/mmdlibdata`. A file at +`data/mmdlib/loot_table/blocks/.json` overrides that block's generated +table, including an empty table from noDrops. Name overrides go under +`assets/mmdlib/lang/en_us.json`; other locales use their normal filenames. The +folder pack remains above the generated pack, so author-supplied resources win. +Material texture generation and directional templates are unchanged. + +## Typed references and module copies + +References are typed IDs, not registered objects. The types are +`mods.mmdlib.ItemReference`, `BlockReference`, `BlockTagReference`, +`ItemModuleReference`, and `BlockModuleReference`. Each takes a string ID and +exposes read-only `id`. They do not query a registry at construction and have no +method for obtaining a live game object. Tag IDs omit `#`. + +Existing string constructors/properties remain supported. These entry points also +accept the corresponding reference: + +| Consumer | Typed entry point | +| --- | --- | +| ItemBuilder / BlockBuilder | second constructor argument: ItemModuleReference / BlockModuleReference | +| ItemModule / BlockModule | second constructor argument: matching module reference | +| ItemBuilder / BlockBuilder | `reference` getter returns ItemReference / BlockReference | +| ItemModule / BlockModule | `reference` getter returns matching module reference | +| MaterialBuilder | `repairWith(ItemReference)`, `baseBlockFrom(BlockReference)`, `incorrectBlocksTag(BlockTagReference)`, `addHarvestTag(BlockTagReference)` | +| MaterialBuilder | `addItemModule(ItemModuleReference)`, `addBlockModule(BlockModuleReference)` | +| ItemBuilder / ItemModule | `remainder(ItemReference)` | +| BlockBuilder | `baseBlockFrom(BlockReference)` | +| BlockBuilder / BlockModule | `addTag(BlockTagReference)` | +| ToolSettings | `mineableTag(BlockTagReference)`, `fastOnTag(BlockTagReference)` | + +Each method unwraps the ID into the existing string-based consumer; it does not +change resolution timing. Wrong reference kinds have no matching overload. +Malformed IDs fail at reference construction. Modules must exist in the current +catalog when selected; constructing a module reference alone does not register or +validate a module. Item/block references may be created before their targets exist, +but actual dependencies still obey the existing runtime registration order and +missing-entry checks. No general forward-resolution guarantee is added. + +Both ItemModule and BlockModule provide `copy(newName)`. The source need not be +built. This snapshots the current factory, template, factory data, tags, companion +metadata, and block material-default setting. Later edits apply only to the chosen +builder. New identity and resource ancestry are preserved separately; aliases are +not inherited. Copying to the source's own ID fails. Duplicate destination modules +still fail under the existing build validation. + +```zenscript +#loader mmdlib +import mods.mmdlib.ItemModule; +import mods.mmdlib.ItemModuleReference; +import mods.mmdlib.ItemBuilder; + +var base = new ItemModule("small_part", new ItemModuleReference("mmdlib:ingot")); +base.stackSize = 16; +var large = base.copy("large_part"); +large.stackSize = 4; +base.build(); +large.build(); +new ItemBuilder("small_widget", base.reference).build(); +new ItemBuilder("large_widget", large.reference).build(); +``` + +`copy` gives locally attached onXxx handlers new callback IDs and separate handler +maps. Replacing a source handler or removing/changing bindings on one copy does +not change the other's binding. The script function object itself is retained; +mutable variables captured by that function are still shared. Explicit callback +IDs supplied with addEvent remain shared references. Constructing a new module +from an already registered module copies those callback IDs; use `copy` on the +source builder when an independent snapshot of its locally owned handlers is +needed. Class-only block-hook restrictions carry over to copies. + +This API copies supported MMDLib module configuration. It does not clone arbitrary +registered vanilla/modded objects, provide a Java subclass, or copy standalone +builder runtime overrides. Modules built earlier in the same script run supply +their item tags to subsequent module and item builders. + +## Generic tool configuration + +`mods.mmdlib.ToolSettings` edits existing `mmdlib:tool` factory settings. +`configureTool(settings)` is available on ItemBuilder and ItemModule and rejects a +currently selected factory other than `mmdlib:tool`. Configure the factory before +calling it. Normal planning still validates the resulting factory data at build. + +```zenscript +#loader mmdlib +import mods.mmdlib.ItemModule; +import mods.mmdlib.ToolSettings; +import mods.mmdlib.MaterialBuilder; + +var module = new ItemModule("custom_hammer", "mmdlib:crackhammer"); +var tool = new ToolSettings(); +tool.mineable = "minecraft:mineable/pickaxe"; +tool.fastOn = "minecraft:mineable/pickaxe"; +tool.attackDamage = 4; +tool.attackSpeed = -2.5; +tool.abilities("pickaxe"); +tool.durability(0.5, 1, ""); +tool.speed(0.5, 1, ""); +module.configureTool(tool); +module.build(); + +var material = new MaterialBuilder("custom_metal"); +material.removeItems("all").addItems("custom_hammer"); +material.removeBlocks("all"); +material.hasFluid = false; +material.repairItem = "minecraft:diamond"; +material.incorrectBlocks = "minecraft:incorrect_for_wooden_tool"; +material.toolDurability(200).toolEfficiency(8).attackDamage(3).enchantability(17); +material.build(); +``` + +The helper has write-only `mineable`, `fastOn`, `attackDamage`, and `attackSpeed` +properties. Tag values are IDs without `#`. `abilities(names...)` replaces the +ability list (no arguments clears it); names use the existing runtime decoder's +vanilla sets or NeoForge ability names. `durability(factor, minimum, stat)` and +`speed(factor, minimum, stat)` replace their respective scaling records. An empty +stat means no material-special override. A named special stat overrides the scaled +value when present. Factors/minima are nonnegative and finite; durability minimum +is at least 1. + +Only explicitly supplied keys replace module factory data; unrelated keys survive. +Scaling records and ability lists replace whole records/lists. Helper values are +snapshotted when applied; editing the helper later does not change a built item. +A subsequent `factoryData = ...` replaces the entire map as before. The existing +runtime codec checks the merged tool data, including the required mineable tag. +This helper does not configure the separate vanilla pickaxe/sword factories. + +**Existing runtime limitation:** the generic tool computes scaled durability, but +its vanilla `TieredItem` superclass then replaces it with the material tier's uses. +Consequently `durability(...)` currently stores/validates the factory configuration +but does not change the final item durability. Use material `toolDurability(...)` +for effective durability. Speed scaling does apply. Fixing the constructor behavior +requires a core change and is deferred by this integration-only plan. + ## Redstone A block built by `mmdlib:block` (the default module) can react to redstone, emit it, @@ -459,16 +676,24 @@ items, blocks, and their resources are created at startup on both sides. ## Not covered yet -* Material events, runtime settings (sound, map color, block set type), and parent - materials. These need the material file format for now. +* Material-wide event bindings and parent materials still need the material file + format. Material runtime properties listed above are exposed in scripts. * Hooks without a contract yet: items ticking in an inventory, crafting and smelting, tossing and picking up, tooltips, burn time, entities inside a block, falling on a block. Each needs a contract like the others. The class-only ones (inventory tick, entity inside, fall) would work on MMDLib's own classes: every item, and the plain block. -* Item template fields that are kept but not applied to registered items yet: lore, - food, use time and animation, attribute modifiers, and tool actions. Scripts do not - expose them until they work. +* Food references, lore, general attribute modifiers, declared tool actions, + delayed-use `on_complete`, and custom creative-stack declarations are retained in + definitions but have no consumer in the standard runtime. Scripts do not expose + setters for these fields. Native tool attributes and generic-tool abilities are + separate supported paths. +* Use duration and animation are exposed and applied by the basic-item factory. + Other item factories keep their native use behavior; these properties do not + turn an arbitrary sword or tool into an item used over time. +* Rarity, feature flags, advanced block-property predicates, custom sound events, + independent tool tiers, and user-defined creative tabs have no matching exposed + definition path. The registrar supplies its existing materials tab. ## Implementation @@ -477,3 +702,18 @@ finds its plugin and script classes by annotation. The only reference from the r of MMDLib is the `CraftTweakerInit.register(modBus)` call in MMDLib's constructor, made only when CraftTweaker is loaded. That call adds a code material source to `MaterialExtensions.GLOBAL`; see [startup definition loading](definition-loading.md#stages). + +## Verification and executable examples + +See the [verification report](crafttweaker-verification.md) for reproducible server, +unit, packaging, and real-client checks, plus inspected screenshots. Tested scripts +cover material/tool settings, typed references, copies, builder aliases, and loot. +The client smoke uses isolated worlds and demonstrates generated English names, +models, material textures, and four horizontal block facings, with separate launches +with and without CraftTweaker. Software rendering is used for the documented +headless environment; normal hardware drivers are not covered by that run. + +The [parity plan](crafttweaker-parity-plan.md) and +[API inventory](crafttweaker-api-inventory.md) retain the boundary between supported +features, integration conveniences, and deferred core work. In particular, per-tool +durability scaling is still limited by TieredItem; material durability works. diff --git a/docs/roadmap.md b/docs/roadmap.md index c0534806..1bb5cf70 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,5 +1,50 @@ # Roadmap +## TODO: food and creative tabs + +These are follow-up work for Java, JSON, and CraftTweaker users, outside the +current [CraftTweaker parity implementation](crafttweaker-parity-plan.md). +The policy choices below remain open; these items are not implemented or verified. + +- [ ] **Food definitions and consumption.** Complete the stored food reference's + path from definition to actual item behavior using one public API shared by + Java, JSON, and CraftTweaker. Add reusable food definitions and consider inline + definitions, retaining existing ID-based declarations. Cover nutrition, + saturation, always-edible behavior, eating duration, consumption remainder, and + effects/probabilities. Resolve and validate registry references through the + normal loading path; apply resolved food properties during item construction. + Add CraftTweaker builders/accessors over that public functionality, and exercise + the same API with MMDLib-owned example content. + **Decisions needed:** saturation value versus multiplier; conflicting timed-use + settings; supported item factories; definition ownership/loading; distinguish + crafting remainders from consumption remainders. + **Verification:** equivalent Java/JSON/script definitions; invalid/missing + reference errors; server consumption tests for hunger, saturation, fullness, + quantity, duration, remainders, effects, and use callbacks; real-client eating + and animation checks. Storing a food ID alone does not satisfy this item. + **Source:** [ItemBuilder.setFood](../src/main/java/zone/moddev/mc/lib/common/builders/ItemBuilder.java), + [ItemTemplate](../src/main/java/zone/moddev/mc/lib/common/codecs/ItemTemplate.java), + [RuntimeProperties.item](../src/main/java/zone/moddev/mc/lib/common/runtime/RuntimeProperties.java), + [StandardItems.Basic](../src/main/java/zone/moddev/mc/lib/common/runtime/StandardItems.java). + +- [ ] **Creative-tab membership and custom tabs.** First apply existing `group` + and `creative_menu_stacks` declarations to the requested vanilla or modded tabs. + Then add public custom-tab definitions/registration for Java and JSON, with + CraftTweaker wrappers using that same path: ID, translated title, icon stack, + contents, relative placement, and basic presentation. Use the public mechanism + for MMDLib's own materials tab. + **Decisions needed:** duplicate entries, ordering, visibility, missing tab IDs, + default materials-tab membership for explicitly assigned items, and modern + component-aware stack definitions versus the existing legacy `tag` field. + Keep tab registration at startup; do not imply `/reload` can add registry IDs. + **Verification:** equivalent Java/JSON/script tab definitions and membership; + invalid-reference and duplicate/order tests; real-client inspection of vanilla, + modded, and custom tabs, titles/icons, component-bearing stacks, and default-tab + behavior; launch without CraftTweaker to verify optional integration. + **Source:** [ItemTemplate](../src/main/java/zone/moddev/mc/lib/common/codecs/ItemTemplate.java), + [MMDItem.CreativeStack](../src/main/java/zone/moddev/mc/lib/common/types/MMDItem.java), + [RuntimeContentRegistrar materials tab](../src/main/java/zone/moddev/mc/lib/common/runtime/RuntimeContentRegistrar.java). + ## Remaining MMDLib 1.0 parity - **GUI gameplay and visual review.** The GUI runtime is implemented; validate addon-specific diff --git a/gradle/verification.gradle b/gradle/verification.gradle index 13b35360..c3be6bfe 100644 --- a/gradle/verification.gradle +++ b/gradle/verification.gradle @@ -57,6 +57,7 @@ tasks.named('check') { dependsOn gameTestTask } // Unit tests in src/test: JUnit 5, no running game. `./gradlew test` (part of `check`). dependencies { + testRuntimeOnly "com.blamejared.crafttweaker:CraftTweaker-neoforge-1.21.1:${crafttweaker_version}" testImplementation 'org.junit.jupiter:junit-jupiter:5.12.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/scripts/crafttweaker-smoke.init.gradle b/scripts/crafttweaker-smoke.init.gradle new file mode 100644 index 00000000..432aa87d --- /dev/null +++ b/scripts/crafttweaker-smoke.init.gradle @@ -0,0 +1,28 @@ +// xvfb-run -a ./gradlew --offline runClient -I scripts/crafttweaker-smoke.init.gradle +// Repeat with -PwithoutCraftTweaker. Uses isolated worlds and scripts under build/. +// Run normal check afterward to restore main output and verify release packaging. +allprojects { + afterEvaluate { + if (extensions.findByName('neoForge') != null) { + sourceSets.main.java.srcDir(file('src/clientSmoke/java')) + def absent = providers.gradleProperty('withoutCraftTweaker').isPresent() + def smokeDir = file(absent ? 'build/ct-smoke-absent' : 'build/ct-smoke-present') + neoForge.runs.client.gameDirectory = smokeDir + neoForge.runs.client.programArguments.set(['--width', '1280', '--height', '960']) + neoForge.runs.client.systemProperty 'mmdlib.craftTweakerSmoke', 'true' + tasks.named('runClient') { + doFirst { + smokeDir.mkdirs() + new File(smokeDir, 'options.txt').text = 'guiScale:2\nmaxFps:30\nonboardAccessibility:false\npauseOnLostFocus:false\nrenderDistance:5\n' + project.delete(new File(smokeDir, 'scripts')) + if (!absent) project.copy { from 'src/clientSmoke/scripts'; into new File(smokeDir, 'scripts') } + } + doLast { + def report = new File(smokeDir, 'logs/latest.log') + if (!report.exists() || !report.text.contains('MMDLIB_CT_CLIENT_PASS') || report.text.contains('MMDLIB_CT_CLIENT_FAIL')) + throw new GradleException('CraftTweaker client smoke failed; inspect ' + report) + } + } + } + } +} diff --git a/src/clientSmoke/README.md b/src/clientSmoke/README.md new file mode 100644 index 00000000..ebaf0d21 --- /dev/null +++ b/src/clientSmoke/README.md @@ -0,0 +1,25 @@ +# CraftTweaker real-client smoke fixtures + +These sources are excluded from normal source sets. Only +`scripts/crafttweaker-smoke.init.gradle` injects the Java smoke class into main for +a development launch. Run a normal `./gradlew --offline check build` afterward to +restore normal outputs and verify no smoke classes reach the release JAR. + +The present-mod run copies only this directory's scripts to an isolated build +folder. It checks startup registrations, English labels, baked model/texture +availability, and front-face model rotation for four horizontal facings. It opens +an integrated world and captures actual world rendering and an item/name gallery. +The absent-mod run copies no scripts and checks the script-only item is absent, +then creates an integrated world and exits successfully. Both runs use a bounded +timeout and explicit pass/fail log markers. + +Texture provenance (test fixtures only): + +- `client_gate.png`: copied from MMDLib's existing copper_block.png. +- `client_gate_front.png` and `client_gate_top.png`: vanilla Minecraft 1.21.1 + furnace_front.png and furnace_top.png, extracted from cached Minecraft resources. +- `client_widget.png`: MMDLib's existing grayscale gear template; see + src/main/resources/mmdlib_templates/provenance.json for its Base Metals source. + +The distinct front/top/side textures make directional rotation visible. These +fixtures are not intended as production artwork or additional release content. diff --git a/src/clientSmoke/java/zone/moddev/mc/lib/smoke/CraftTweakerClientSmoke.java b/src/clientSmoke/java/zone/moddev/mc/lib/smoke/CraftTweakerClientSmoke.java new file mode 100644 index 00000000..71b5e232 --- /dev/null +++ b/src/clientSmoke/java/zone/moddev/mc/lib/smoke/CraftTweakerClientSmoke.java @@ -0,0 +1,144 @@ +package zone.moddev.mc.lib.smoke; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.Screenshot; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.RandomSource; +import net.minecraft.world.Difficulty; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.*; +import net.minecraft.world.level.levelgen.WorldOptions; +import net.minecraft.world.level.levelgen.presets.WorldPresets; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModList; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.RenderFrameEvent; +import zone.moddev.mc.lib.MMDLib; +import zone.moddev.mc.lib.common.runtime.RedstoneBlock; + +/** Opt-in real-client validation; injected only by crafttweaker-smoke.init.gradle. */ +@EventBusSubscriber(modid = "mmdlib", value = Dist.CLIENT) +public final class CraftTweakerClientSmoke { + private static boolean started, requested, finished; + private static volatile boolean ready; + private static int ticks, frames, stage; + private static final long DEADLINE = System.currentTimeMillis() + 240_000; + private static final BlockPos POS = new BlockPos(0, 100, 0); + private static final String[] ITEMS = {"client_alloy_ingot", "client_alloy_pickaxe", "client_alloy_block", "client_gate", "client_widget"}; + + private static ResourceLocation id(String path) { return ResourceLocation.parse("mmdlib:" + path); } + private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); } + private static boolean present() { return ModList.get().isLoaded("crafttweaker"); } + + @SubscribeEvent + public static void tick(ClientTickEvent.Post event) { + if (!Boolean.getBoolean("mmdlib.craftTweakerSmoke") || finished) return; + var mc = Minecraft.getInstance(); + try { + require(System.currentTimeMillis() < DEADLINE, "client smoke timed out: " + mc.screen); + if (!started && mc.screen instanceof TitleScreen) { + started = true; + require(BuiltInRegistries.ITEM.containsKey(id("client_widget")) == present(), "optional integration registry boundary"); + var settings = new LevelSettings("CT smoke", GameType.CREATIVE, false, Difficulty.PEACEFUL, true, new GameRules(), WorldDataConfiguration.DEFAULT); + mc.createWorldOpenFlows().createFreshLevel("ct-smoke-" + System.currentTimeMillis(), settings, + new WorldOptions(1, false, false), r -> r.registryOrThrow(Registries.WORLD_PRESET) + .getHolderOrThrow(WorldPresets.FLAT).value().createWorldDimensions(), mc.screen); + } + if (mc.player == null || mc.level == null) return; + if (!requested) { + requested = true; + mc.getSingleplayerServer().execute(() -> { + try { + var player = mc.getSingleplayerServer().getPlayerList().getPlayers().getFirst(); + var level = player.serverLevel(); + level.setDayTime(6000); + for (int x = -6; x <= 6; x++) for (int z = -2; z <= 8; z++) + level.setBlock(POS.offset(x, -1, z), net.minecraft.world.level.block.Blocks.STONE.defaultBlockState(), 3); + if (present()) { + int x = -3; + for (var direction : Direction.Plane.HORIZONTAL) { + level.setBlock(POS.offset(x, 0, 0), BuiltInRegistries.BLOCK.get(id("client_gate")) + .defaultBlockState().setValue(RedstoneBlock.FACING, direction), 3); + x += 2; + } + level.setBlock(POS.offset(0, 0, -2), BuiltInRegistries.BLOCK.get(id("client_alloy_block")).defaultBlockState(), 3); + } + player.teleportTo(.5, 103, 7.5); + player.setYRot(180); player.setXRot(22); + ready = true; + } catch (Throwable error) { fail(error); } + }); + } + if (!ready || ++ticks < 100 || stage != 0) return; + if (present()) { + require(new ItemStack(BuiltInRegistries.ITEM.get(id("client_widget"))).getHoverName().getString().equals("Named Test Widget"), "English name override"); + require(new ItemStack(BuiltInRegistries.ITEM.get(id("client_alloy_ingot"))).getHoverName().getString().equals("Amber Alloy Ingot"), "material English name"); + for (String name : ITEMS) { + var model = mc.getItemRenderer().getModel(new ItemStack(BuiltInRegistries.ITEM.get(id(name))), mc.level, mc.player, 0); + require(model != mc.getModelManager().getMissingModel(), "missing item model: " + name); + require(!model.getParticleIcon().contents().name().getPath().equals("missingno"), "missing texture: " + name); + } + var block = BuiltInRegistries.BLOCK.get(id("client_gate")); + for (var direction : Direction.Plane.HORIZONTAL) { + var state = block.defaultBlockState().setValue(RedstoneBlock.FACING, direction); + var model = mc.getBlockRenderer().getBlockModel(state); + require(model.getQuads(state, direction, RandomSource.create(1)).stream() + .anyMatch(q -> q.getSprite().contents().name().equals(id("block/client_gate_front"))), "front model rotation: " + direction); + } + } + mc.options.hideGui = true; + mc.player.setYRot(180); mc.player.setXRot(22); + mc.setScreen(null); + stage = 1; frames = 0; + } catch (Throwable error) { fail(error); } + } + + @SubscribeEvent + public static void frame(RenderFrameEvent.Post event) { + if (!Boolean.getBoolean("mmdlib.craftTweakerSmoke") || finished || stage == 0) return; + var mc = Minecraft.getInstance(); + try { + if (++frames < 30) return; + if (stage == 1) { + Screenshot.grab(mc.gameDirectory, "ct-directional-world.png", mc.getMainRenderTarget(), message -> {}); + if (present()) { mc.setScreen(new Gallery()); stage = 2; frames = 0; return; } + } else Screenshot.grab(mc.gameDirectory, "ct-items-and-names.png", mc.getMainRenderTarget(), message -> {}); + MMDLib.LOGGER.info("MMDLIB_CT_CLIENT_PASS: CraftTweaker installed={}, integrated world, resource checks and captures complete", present()); + finished = true; + mc.stop(); + } catch (Throwable error) { fail(error); } + } + + private static void fail(Throwable error) { + finished = true; + MMDLib.LOGGER.error("MMDLIB_CT_CLIENT_FAIL", error); + Minecraft.getInstance().execute(() -> Minecraft.getInstance().stop()); + } + + private static final class Gallery extends Screen { + Gallery() { super(Component.literal("CraftTweaker content smoke")); } + @Override public boolean isPauseScreen() { return false; } + @Override public void render(GuiGraphics g, int mouseX, int mouseY, float partialTick) { + g.fill(0, 0, width, height, 0xFF26313C); + g.drawString(font, title, 30, 25, 0xFFFFFFFF); + int y = 65; + for (String name : ITEMS) { + var stack = new ItemStack(BuiltInRegistries.ITEM.get(id(name))); + g.pose().pushPose(); g.pose().translate(35, y, 0); g.pose().scale(2, 2, 2); + g.renderItem(stack, 0, 0); g.pose().popPose(); + g.drawString(font, stack.getHoverName(), 90, y + 10, 0xFFFFFFFF); + y += 62; + } + } + } +} diff --git a/src/clientSmoke/scripts/content.zs b/src/clientSmoke/scripts/content.zs new file mode 100644 index 00000000..34e2df2a --- /dev/null +++ b/src/clientSmoke/scripts/content.zs @@ -0,0 +1,22 @@ +#loader mmdlib +import mods.mmdlib.MaterialBuilder; +import mods.mmdlib.BlockBuilder; +import mods.mmdlib.ItemBuilder; +var alloy = new MaterialBuilder("client_alloy"); +alloy.displayName = "Amber Alloy"; +alloy.color = 0xFFFFA020; +alloy.strength = 8; +alloy.hardness = 6; +alloy.hasFluid = false; +alloy.removeItems("all").addItems("ingot", "pickaxe"); +alloy.removeBlocks("all").addBlocks("block"); +alloy.build(); +var gate = new BlockBuilder("client_gate"); +gate.displayName = "Directional Test Gate"; +gate.directional = true; +gate.redstoneOutput = true; +gate.build(); +var widget = new ItemBuilder("client_widget"); +widget.displayName = "Named Test Widget"; +widget.stacksTo(16); +widget.build(); diff --git a/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate.png b/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate.png new file mode 100644 index 0000000000000000000000000000000000000000..fa14972f68ccaf123b507e946f444d6b3cab524a GIT binary patch literal 329 zcmV-P0k-~$P)LFW{iyOZ$eZytr~znb z+PQj_M$9wdwXXmyq(SSH`Q-w5?rDHO&v5ezGv&>!{d?*vfDDJG#IVBaCN^KDdH>jn bfOB*JzGmkGEk6If00000NkvXXu0mjf*B*>; literal 0 HcmV?d00001 diff --git a/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate_front.png b/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate_front.png new file mode 100644 index 0000000000000000000000000000000000000000..a529b6c491ebb58359bc163ded8fb499b5041b0c GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFv5AX?bJ$m%$h7B85tXMI3?%atJ zC$_e>mY0`jWMsrfM+f-%*;rdEDk=&J3QkJgVFT1M$EPj=d{yI4`4kPj#_OJfo+9 z!u2C-D%)n6zF~Q3^7M){^Nl^8Y5_+%-Ux~&&WKo-&Odp*~`o;xjgNI}HmB6k!+%Sk(l!rXnVEO)FYKEDD>E2)a*7Z>NAt%`ZHa%eGE<0a?n!e)JyiT8vn-=D zO<&)vK>WC_i^g#zPR#RMLHHf8IF8K_e8g&+hN`Nfq9{}V!XQMFB<@|D=cxdQvMe8f z+>N#Owry#iXIhqp`o4d)!T|Q<0pS6axGIONB>i9?qA2?2;DTPeu2UZ9VhLdnAbtR{ zg}Sb37zQf<;(g!AI8NRPJFmBJ-#1OuMBBER#nNqk{>O1BF&04Uy1pUYpK0c77$OWq zyH8wAnx%_nj`KXQn3Ew6g5bA tags; private final Map events; private final Map hooks = new java.util.HashMap<>(); + private final Map> hookCopiers = new java.util.HashMap<>(); private JsonObject factoryData; ScriptDefinitionProperties( @@ -63,6 +72,10 @@ void hook( var id = definitionId().withSuffix("/" + event.name()); boolean first = !hooks.containsKey(event.name()); hooks.put(event.name(), java.util.Objects.requireNonNull(handler)); + hookCopiers.put(event.name(), (target, value) -> { + @SuppressWarnings("unchecked") H snapshot = (H) value; + target.hook(event, snapshot, invoke); + }); if (first) ScriptDefinitions.catalog() .events() @@ -81,6 +94,14 @@ void hook( addEvent(event.name(), id.toString()); } + /** Rebind active owned hooks with independent maps; explicit external callbacks stay shared. */ + void copyHooksTo(ScriptDefinitionProperties target) { + hooks.forEach((event, handler) -> { + if (definitionId().withSuffix("/" + event).equals(events.get(event))) + hookCopiers.get(event).accept(target, handler); + }); + } + /** Settings for the Java implementation, such as {@code {detects: "players"}}. */ @ZenCodeType.Getter("factoryData") public IData getFactoryData() { diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitions.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitions.java index 2a5d8947..a573896b 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitions.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitions.java @@ -43,8 +43,23 @@ final class ScriptDefinitions { private static volatile Set itemIds = Set.of(); private static volatile Map> builtItemTags = Map.of(); private static volatile Map builtBasedOn = Map.of(); + private static final Map resources = new LinkedHashMap<>(); + private static volatile Map builtResources = Map.of(); private static MaterialExtensions catalog; + static ScriptResourceOptions resourceOptions(String kind, ResourceLocation id) { + return resourceOptions().getOrDefault(ScriptResourceOptions.key(kind, id), ScriptResourceOptions.DEFAULT); + } + + static Map resourceOptions() { + return catalog == null ? builtResources : Map.copyOf(resources); + } + + static void resources(String kind, ResourceLocation id, ScriptResourceOptions options) { + catalog(); + resources.put(ScriptResourceOptions.key(kind, id), options); + } + private ScriptDefinitions() {} /** Runs the scripts against {@code api} and returns what they built. */ @@ -56,6 +71,7 @@ static synchronized MaterialExtensions.Definitions load(MaterialExtensions api) items.clear(); itemTags.clear(); basedOn.clear(); + resources.clear(); catalog = api; try { CraftTweakerAPI.getScriptRunManager() @@ -82,6 +98,7 @@ static synchronized MaterialExtensions.Definitions load(MaterialExtensions api) itemIds = Set.copyOf(items.keySet()); builtItemTags = Map.copyOf(itemTags); builtBasedOn = Map.copyOf(basedOn); + builtResources = Map.copyOf(resources); var result = new MaterialExtensions.Definitions(materials.values(), blocks, items); materials.clear(); blocks.clear(); @@ -149,7 +166,7 @@ static synchronized void module( /** Item tags of script items and item modules, by item or module ID. */ static Map> itemTags() { - return builtItemTags; + return catalog == null ? builtItemTags : Map.copyOf(itemTags); } /** The module each script module was copied from. */ diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java index c790a645..40b07a1b 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java @@ -51,12 +51,21 @@ public ScriptItemBuilder(String name, String module) { this(mmdlibId(name, "item"), ResourceLocation.parse(module)); } + @ZenCodeType.Constructor + public ScriptItemBuilder(String name, ScriptItemModuleReference module) { + this(name, module.getId()); + } + + @ZenCodeType.Getter("reference") + public ScriptItemReference getReference() { return new ScriptItemReference(id.toString()); } + private ScriptItemBuilder(ResourceLocation id, ResourceLocation module) { super( ScriptDefinitions.catalog().itemModule(module).template(), FactoryData.EMPTY, ScriptDefinitions.itemTags().getOrDefault(module, Set.of())); this.id = id; + resourceOptions = ScriptDefinitions.resourceOptions("item_module", module); this.module = module; } @@ -104,5 +113,6 @@ public void build() { template.toBuilder().setName(module).forMaterial(id).setRegistryName(id).build(); ScriptDefinitions.add( id, new StandaloneItem(module, factoryData(), Optional.of(template), runtime), tags()); + ScriptDefinitions.resources("item", id, resourceOptions); } } diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java index 3ab952cb..37c6ea1d 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java @@ -52,6 +52,30 @@ public ScriptItemModule(String name, String basedOn) { this(mmdlibId(name, "item module"), ResourceLocation.parse(basedOn)); } + @ZenCodeType.Constructor + public ScriptItemModule(String name, ScriptItemModuleReference basedOn) { + this(name, basedOn.getId()); + } + + @ZenCodeType.Getter("reference") + public ScriptItemModuleReference getReference() { + return new ScriptItemModuleReference(id.toString()); + } + + /** Snapshot current configuration under a new identity; source need not be built. */ + @ZenCodeType.Method + public ScriptItemModule copy(String name) { + var targetId = mmdlibId(name, "item module"); + if (targetId.equals(id)) throw new IllegalArgumentException("Copy needs a different module ID"); + var snapshot = new DataModules.Item(factory, base.requiredMod(), + factoryData(), template(), base.part()); + var target = new ScriptItemModule(targetId, baseId, snapshot); + target.removeTags("all"); target.addTags(tags().stream().map(Object::toString).toArray(String[]::new)); + target.resourceOptions = resourceOptions; + copyHooksTo(target); + return target; + } + private ScriptItemModule(ResourceLocation id, ResourceLocation baseId) { this(id, baseId, ScriptDefinitions.catalog().itemModule(baseId)); } @@ -59,6 +83,7 @@ private ScriptItemModule(ResourceLocation id, ResourceLocation baseId) { private ScriptItemModule(ResourceLocation id, ResourceLocation baseId, DataModules.Item base) { super(base.template(), base.factoryData(), ScriptDefinitions.itemTags().getOrDefault(baseId, Set.of())); this.id = id; + resourceOptions = ScriptDefinitions.resourceOptions("item_module", baseId); this.baseId = baseId; this.base = base; factory = base.factory(); @@ -106,6 +131,7 @@ public void build() { factoryData(), template(), new PartMetadata(List.of(), part.entityFactory(), part.entityData(), part.blockEntity()))); + ScriptDefinitions.resources("item_module", id, resourceOptions); ScriptDefinitions.module(id, baseId, tags()); } } diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModuleReference.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModuleReference.java new file mode 100644 index 00000000..b6fb3cf5 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModuleReference.java @@ -0,0 +1,18 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; +import net.minecraft.resources.ResourceLocation; +import org.openzen.zencode.java.ZenCodeType; + +/** A typed ID, never an eager registry lookup. The existing consumer resolves it later. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ItemModuleReference") +public final class ScriptItemModuleReference { + private final ResourceLocation id; + + @ZenCodeType.Constructor + public ScriptItemModuleReference(String id) { this.id = ResourceLocation.parse(id); } + + @ZenCodeType.Getter("id") + public String getId() { return id.toString(); } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java index 8cf83eb6..f1fa4b5a 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java @@ -188,6 +188,39 @@ public void setContainer(String container) { container.isEmpty() ? Optional.empty() : Optional.of(ResourceLocation.parse(container)); } + /** Configure only an item/module whose selected implementation is mmdlib:tool. */ + @ZenCodeType.Method + public ScriptItemProperties configureTool(ScriptToolSettings settings) { + var factory = this instanceof ScriptItemModule module + ? module.getFactory() + : ScriptDefinitions.catalog().itemModule( + ResourceLocation.parse(((ScriptItemBuilder) this).getModule())).factory().toString(); + if (!factory.equals("mmdlib:tool")) + throw new IllegalArgumentException("configureTool requires factory mmdlib:tool, not " + factory); + var merged = factoryData().json().getAsJsonObject(); + settings.json().entrySet().forEach(entry -> merged.add(entry.getKey(), entry.getValue())); + new FactoryData(merged).decode(zone.moddev.mc.lib.common.runtime.StandardItems.ToolSettings.CODEC) + .getOrThrow(message -> new IllegalArgumentException("Invalid tool settings: " + message)); + setFactoryData(com.blamejared.crafttweaker.api.data.converter.JSONConverter.convert(merged)); + return this; + } + + @ZenCodeType.Method + public ScriptItemProperties remainder(ScriptItemReference item) { + setContainer(item.getId()); return this; + } + + @ZenCodeType.Method + public ScriptItemProperties stacksTo(int size) { setStackSize(size); return this; } + + @ZenCodeType.Method + public ScriptItemProperties craftRemainder(String item) { setContainer(item); return this; } + + @ZenCodeType.Method + public ScriptItemProperties craftRemainder(ScriptItemReference item) { + return craftRemainder(item.getId()); + } + /** The template these values describe. */ ItemTemplate template() { return new ItemTemplate( diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemReference.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemReference.java new file mode 100644 index 00000000..81e38f48 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemReference.java @@ -0,0 +1,18 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; +import net.minecraft.resources.ResourceLocation; +import org.openzen.zencode.java.ZenCodeType; + +/** A typed ID, never an eager registry lookup. The existing consumer resolves it later. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ItemReference") +public final class ScriptItemReference { + private final ResourceLocation id; + + @ZenCodeType.Constructor + public ScriptItemReference(String id) { this.id = ResourceLocation.parse(id); } + + @ZenCodeType.Getter("id") + public String getId() { return id.toString(); } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialBuilder.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialBuilder.java index 4b628418..bc04599f 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialBuilder.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialBuilder.java @@ -54,6 +54,7 @@ public final class ScriptMaterialBuilder { private final Set blocks; private final Set harvestTags = new LinkedHashSet<>(); private final Map special = new LinkedHashMap<>(); + private String displayName = ""; private String type = MaterialProperties.DEFAULT.type(); private int color = MaterialProperties.DEFAULT.color(); private double hardness; @@ -63,6 +64,13 @@ public final class ScriptMaterialBuilder { private boolean hasFluid; private ResourceLocation fluid; + private java.util.Optional repairItem = MaterialRuntimeSettings.DEFAULT.repairItem(); + private ResourceLocation incorrectBlocks = MaterialRuntimeSettings.DEFAULT.incorrectBlocks(); + private ResourceLocation baseBlock = MaterialRuntimeSettings.DEFAULT.baseBlock(); + private ResourceLocation blockSetType = MaterialRuntimeSettings.DEFAULT.blockSetType(); + private java.util.Optional blockSound = MaterialRuntimeSettings.DEFAULT.blockSound(); + private java.util.Optional mapColor = MaterialRuntimeSettings.DEFAULT.mapColor(); + /** * Starts a material. The name is a path such as {@code "copper"}; {@code "mmdlib:copper"} is also * accepted, but no other namespace. @@ -305,18 +313,137 @@ public void build() { .setRuntime(runtime()) .build(); ScriptDefinitions.add(material); + if (!displayName.isEmpty()) ScriptDefinitions.resources("material", name, ScriptResourceOptions.DEFAULT.named(displayName)); + } + + @ZenCodeType.Getter("repairItem") + public String getRepairItem() { return repairItem.map(Object::toString).orElse(""); } + + @ZenCodeType.Setter("repairItem") + public void setRepairItem(String value) { + repairItem = value.isEmpty() ? java.util.Optional.empty() : java.util.Optional.of(ResourceLocation.parse(value)); + } + + @ZenCodeType.Getter("incorrectBlocks") + public String getIncorrectBlocks() { return incorrectBlocks.toString(); } + + @ZenCodeType.Setter("incorrectBlocks") + public void setIncorrectBlocks(String value) { + incorrectBlocks = ResourceLocation.parse(value); + } + + @ZenCodeType.Getter("baseBlock") + public String getBaseBlock() { return baseBlock.toString(); } + + @ZenCodeType.Setter("baseBlock") + public void setBaseBlock(String value) { + baseBlock = ResourceLocation.parse(value); + } + + @ZenCodeType.Getter("blockSetType") + public String getBlockSetType() { return blockSetType.toString(); } + + @ZenCodeType.Setter("blockSetType") + public void setBlockSetType(String value) { + blockSetType = ResourceLocation.parse(value); + } + + @ZenCodeType.Getter("blockSound") + public String getBlockSound() { return blockSound.map(Object::toString).orElse(""); } + + @ZenCodeType.Setter("blockSound") + public void setBlockSound(String value) { + if (!value.isEmpty()) { + try { net.minecraft.world.level.block.SoundType.class.getField(value.toUpperCase(java.util.Locale.ROOT)); } + catch (ReflectiveOperationException ex) { throw new IllegalArgumentException("Unknown blockSound: " + value, ex); } + } + blockSound = value.isEmpty() ? java.util.Optional.empty() : java.util.Optional.of(value); + } + + @ZenCodeType.Getter("mapColor") + public String getMapColor() { return mapColor.map(Object::toString).orElse(""); } + + @ZenCodeType.Setter("mapColor") + public void setMapColor(String value) { + if (!value.isEmpty()) { + try { net.minecraft.world.level.material.MapColor.class.getField(value.toUpperCase(java.util.Locale.ROOT)); } + catch (ReflectiveOperationException ex) { throw new IllegalArgumentException("Unknown mapColor: " + value, ex); } + } + mapColor = value.isEmpty() ? java.util.Optional.empty() : java.util.Optional.of(value); + } + + @ZenCodeType.Method + public ScriptMaterialBuilder toolDurability(int value) { + if (value < 1) throw new IllegalArgumentException("Invalid toolDurability: " + value); + return addSpecial("tool_durability", value); + } + + @ZenCodeType.Method + public ScriptMaterialBuilder toolEfficiency(double value) { + if (!Double.isFinite(value) || value < 0 || value > Float.MAX_VALUE) throw new IllegalArgumentException("Invalid toolEfficiency: " + value); + return addSpecial("tool_efficiency", value); + } + + @ZenCodeType.Method + public ScriptMaterialBuilder attackDamage(double value) { + if (!Double.isFinite(value) || Math.abs(value) > Float.MAX_VALUE) throw new IllegalArgumentException("Invalid attackDamage: " + value); + return addSpecial("base_attack_damage", value); + } + + @ZenCodeType.Method + public ScriptMaterialBuilder enchantability(int value) { + if (value < 0) throw new IllegalArgumentException("Invalid enchantability: " + value); + return addSpecial("enchantability", value); + } + + @ZenCodeType.Method + public ScriptMaterialBuilder repairWith(ScriptItemReference item) { + setRepairItem(item.getId()); return this; + } + + @ZenCodeType.Method + public ScriptMaterialBuilder baseBlockFrom(ScriptBlockReference block) { + setBaseBlock(block.getId()); return this; + } + + @ZenCodeType.Method + public ScriptMaterialBuilder incorrectBlocksTag(ScriptBlockTagReference tag) { + setIncorrectBlocks(tag.getId()); return this; + } + + @ZenCodeType.Method + public ScriptMaterialBuilder addHarvestTag(ScriptBlockTagReference tag) { + return addHarvestTags(tag.getId()); + } + + @ZenCodeType.Method + public ScriptMaterialBuilder addItemModule(ScriptItemModuleReference module) { + return addItems(module.getId()); + } + + @ZenCodeType.Method + public ScriptMaterialBuilder addBlockModule(ScriptBlockModuleReference module) { + return addBlocks(module.getId()); + } + + @ZenCodeType.Getter("displayName") + public String getDisplayName() { return displayName; } + + @ZenCodeType.Setter("displayName") + public void setDisplayName(String value) { + ScriptResourceOptions.DEFAULT.named(value); + displayName = value; } private MaterialRuntimeSettings runtime() { - var d = MaterialRuntimeSettings.DEFAULT; return new MaterialRuntimeSettings( - d.repairItem(), - d.incorrectBlocks(), - d.baseBlock(), - d.blockSetType(), + repairItem, + incorrectBlocks, + baseBlock, + blockSetType, name, - d.blockSound(), - d.mapColor(), + blockSound, + mapColor, false, false); } diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java index ebb42bd7..0811604b 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java @@ -55,6 +55,13 @@ static SortedMap assets( Map basedOn, MaterialTemplates templates, Path userFolder) { + return assets(materials, blocks, items, basedOn, templates, userFolder, Map.of()); + } + + static SortedMap assets( + Collection materials, Collection blocks, + Collection items, Map basedOn, + MaterialTemplates templates, Path userFolder, Map options) { var files = new TreeMap(); var lang = new TreeMap(); for (var result : materials) { @@ -116,17 +123,18 @@ static SortedMap assets( .read("textures/" + texture + ".png.mcmeta") .ifPresent(meta -> files.put(target + ".mcmeta", meta)); }); - var display = display(name); + var display = options.getOrDefault(ScriptResourceOptions.key("material", + material.name().orElseThrow()), ScriptResourceOptions.DEFAULT).label(display(name), display(name)); plan.items() .forEach( (part, item) -> { if (!plan.blocks().containsKey(part)) - lang.put("item." + NS + "." + item.name().getPath(), label(part.id(), display)); + lang.put("item." + NS + "." + item.name().getPath(), ScriptResourceOptions.find(options, "item", item.name(), "item_module", part.templateId()).label(label(part.id(), display), display)); }); plan.blocks() .forEach( (part, block) -> - lang.put("block." + NS + "." + block.name().getPath(), label(part.id(), display))); + lang.put("block." + NS + "." + block.name().getPath(), ScriptResourceOptions.find(options, "block", block.name(), "block_module", part.templateId()).label(label(part.id(), display), display))); plan.entities() .forEach( (part, entity) -> @@ -143,7 +151,7 @@ static SortedMap assets( .forEach( (part, block) -> { var id = block.name(); - lang.put("block." + id.getNamespace() + "." + id.getPath(), display(id.getPath())); + lang.put("block." + id.getNamespace() + "." + id.getPath(), ScriptResourceOptions.find(options, "block", id, "block_module", part.templateId()).label(display(id.getPath()), display(id.getPath()))); var tpl = templatePart(part, "blockstates/", templates, basedOn) // A plain block that turns to face its placer gets front and top faces. @@ -166,7 +174,7 @@ static SortedMap assets( .forEach( (part, item) -> { var id = item.name(); - lang.put("item." + id.getNamespace() + "." + id.getPath(), display(id.getPath())); + lang.put("item." + id.getNamespace() + "." + id.getPath(), ScriptResourceOptions.find(options, "item", id, "item_module", part.templateId()).label(display(id.getPath()), display(id.getPath()))); var tpl = templatePart(part, "models/item/", templates, basedOn); if (tpl.isEmpty()) { noTemplate("item", id, part); @@ -293,6 +301,13 @@ static SortedMap data( Collection blocks, Collection items, Map> itemTags) { + return data(materials, blocks, items, itemTags, Map.of()); + } + + static SortedMap data( + Collection materials, Collection blocks, + Collection items, Map> itemTags, + Map options) { var files = new TreeMap(); var tags = new TreeMap>(); for (var result : materials) { @@ -375,6 +390,38 @@ static SortedMap data( "data/" + id.getNamespace() + "/tags/" + parts[0] + "/" + id.getPath() + ".json", json(body).getBytes(StandardCharsets.UTF_8)); }); + java.util.stream.Stream.concat(materials.stream(), blocks.stream()).forEach(result -> + result.content().blocks().forEach((part, block) -> { + var id = block.name(); + var option = ScriptResourceOptions.find(options, "block", id, "block_module", part.templateId()); + var path = "data/" + id.getNamespace() + "/loot_table/blocks/" + id.getPath() + ".json"; + switch (option.loot()) { + case "default" -> { + // A double slab intentionally has no own item and drops its matching slabs. + if (block.item() == null && !part.factory().equals(ResourceLocation.parse("mmdlib:double_slab"))) + files.remove(path); + } + case "none" -> files.put(path, "{\"type\":\"minecraft:block\",\"pools\":[]}".getBytes(StandardCharsets.UTF_8)); + case "self" -> { + if (block.item() == null) throw new IllegalArgumentException("dropsSelf requires an item: " + id); + files.put(path, lootTable(ResourceLocation.parse("mmdlib:block"), id.toString(), null, false) + .orElseThrow().getBytes(StandardCharsets.UTF_8)); + } + case "table" -> { + if (option.table().equals(id.withPrefix("blocks/").toString())) + throw new IllegalArgumentException("Loot table cannot reference itself: " + id); + var table = new JsonObject(); + table.addProperty("type", "minecraft:block"); + var pool = new JsonObject(); pool.addProperty("rolls", 1); + var entry = new JsonObject(); entry.addProperty("type", "minecraft:loot_table"); + entry.addProperty("value", option.table()); + var entries = new JsonArray(); entries.add(entry); pool.add("entries", entries); + var pools = new JsonArray(); pools.add(pool); table.add("pools", pools); + files.put(path, json(table).getBytes(StandardCharsets.UTF_8)); + } + default -> throw new IllegalArgumentException("Unknown loot mode: " + option.loot()); + } + })); return files; } diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptResourceOptions.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptResourceOptions.java new file mode 100644 index 00000000..32542133 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptResourceOptions.java @@ -0,0 +1,29 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import java.util.Map; +import net.minecraft.resources.ResourceLocation; + +/** Immutable script-pack metadata, kept outside core definitions. */ +record ScriptResourceOptions(String displayName, String loot, String table) { + static final ScriptResourceOptions DEFAULT = new ScriptResourceOptions("", "default", ""); + + ScriptResourceOptions named(String name) { + if (name.isBlank()) throw new IllegalArgumentException("Display name must not be blank"); + return new ScriptResourceOptions(name, loot, table); + } + + ScriptResourceOptions drops(String mode, String reference) { + return new ScriptResourceOptions(displayName, mode, reference); + } + + static String key(String kind, ResourceLocation id) { return kind + "|" + id; } + + static ScriptResourceOptions find(Map options, String kind, + ResourceLocation id, String moduleKind, ResourceLocation module) { + return options.getOrDefault(key(kind, id), options.getOrDefault(key(moduleKind, module), DEFAULT)); + } + + String label(String fallback, String material) { + return displayName.isEmpty() ? fallback : displayName.replace("__material__", material); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptToolSettings.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptToolSettings.java new file mode 100644 index 00000000..643b5377 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptToolSettings.java @@ -0,0 +1,83 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.blamejared.crafttweaker.api.annotation.ZenRegister; +import com.google.gson.JsonObject; +import net.minecraft.resources.ResourceLocation; +import org.openzen.zencode.java.ZenCodeType; + +/** Typed edits to the existing mmdlib:tool factory data; omitted keys retain module values. */ +@ZenRegister(loaders = ScriptDefinitions.LOADER) +@ZenCodeType.Name("mods.mmdlib.ToolSettings") +public final class ScriptToolSettings { + private final JsonObject data = new JsonObject(); + + @ZenCodeType.Constructor + public ScriptToolSettings() {} + + @ZenCodeType.Setter("mineable") + public void setMineable(String tag) { data.addProperty("mineable", ResourceLocation.parse(tag).toString()); } + + @ZenCodeType.Setter("fastOn") + public void setFastOn(String tag) { data.addProperty("fast_on", ResourceLocation.parse(tag).toString()); } + + @ZenCodeType.Setter("attackDamage") + public void setAttackDamage(float value) { finite(value); data.addProperty("attack_damage", value); } + + @ZenCodeType.Setter("attackSpeed") + public void setAttackSpeed(float value) { finite(value); data.addProperty("attack_speed", value); } + + /** Replaces the ability list, including an empty list to clear it. */ + @ZenCodeType.Method + public ScriptToolSettings abilities(String... names) { + var array = new com.google.gson.JsonArray(); + for (String name : names) { + if (name.isBlank()) throw new IllegalArgumentException("Ability name must not be blank"); + array.add(name); + } + data.add("abilities", array); + return this; + } + + @ZenCodeType.Method + public ScriptToolSettings durability(double factor, int minimum, String stat) { + if (minimum < 1) throw new IllegalArgumentException("Durability minimum must be at least 1"); + data.add("durability", scaled(factor, minimum, stat)); + return this; + } + + @ZenCodeType.Method + public ScriptToolSettings speed(double factor, double minimum, String stat) { + data.add("speed", scaled(factor, minimum, stat)); + return this; + } + + private static JsonObject scaled(double factor, double minimum, String stat) { + finite(factor); finite(minimum); + if (factor < 0 || minimum < 0) throw new IllegalArgumentException("Scale and minimum must be nonnegative"); + var result = new JsonObject(); + result.addProperty("tool_factor", factor); + result.addProperty("minimum", minimum); + if (!stat.isEmpty()) { + if (stat.isBlank()) throw new IllegalArgumentException("Stat must be a name or empty"); + result.addProperty("stat", stat); + } + return result; + } + + private static void finite(double value) { + if (!Double.isFinite(value) || Math.abs(value) > Float.MAX_VALUE) + throw new IllegalArgumentException("Tool value must be finite and fit a float"); + } + + @ZenCodeType.Method + public ScriptToolSettings mineableTag(ScriptBlockTagReference tag) { + setMineable(tag.getId()); return this; + } + + @ZenCodeType.Method + public ScriptToolSettings fastOnTag(ScriptBlockTagReference tag) { + setFastOn(tag.getId()); return this; + } + + JsonObject json() { return data.deepCopy(); } +} diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ContentSmoke.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ContentSmoke.java index 6ee8b4ca..dedea8a8 100644 --- a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ContentSmoke.java +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ContentSmoke.java @@ -38,14 +38,14 @@ private ContentSmoke() {} * from test resources; the hook block from the test script) and the item count. */ static void registered() { - if (MMDLib.loadedMaterials().size() != 15) throw new AssertionError("Missing materials"); + if (MMDLib.loadedMaterials().size() != 16) throw new AssertionError("Missing materials"); var detector = BuiltInRegistries.BLOCK.get(ResourceLocation.parse("mmdlib:human_detector")); - if (MMDLib.standaloneBlocks().size() != 4 + if (MMDLib.standaloneBlocks().size() != 12 || !(detector instanceof StandardBlocks.DetectorPressurePlate)) throw new AssertionError("Standalone player-only plate missing: " + detector); // 234 release items; from test resources the gate's and hook block's block items and the hook - // tool; from the test script the hook block's block item, the hook tool and the hammer. - if (registrar().items().size() != 240) + // tool; from the test script the hook block's block item, the hook tool, the hammer, the phase 2 material tool, and seven phase 3 entries. + if (registrar().items().size() != 254) throw new AssertionError("Wrong runtime item count: " + registrar().items().size()); } @@ -69,7 +69,7 @@ static void blocksPlace(ServerLevel level, BlockPos floor) { placed++; } // 59 release blocks, the test gate, the hook block and the script hook block. - if (placed != 62) throw new AssertionError("Wrong placed block count: " + placed); + if (placed != 69) throw new AssertionError("Wrong placed block count: " + placed); } /** Arrows and bolts keep their pickup item and its components across save and load. */ diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java index bd1bcb97..64fe4605 100644 --- a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/MMDLibGameTests.java @@ -58,6 +58,21 @@ public static void processorIdleSearchBackoff(GameTestHelper helper) { private MMDLibGameTests() {} + @GameTest(template = EMPTY) + public static void scriptGeneratedLoot(GameTestHelper helper) { + ScriptHookSmoke.generatedLoot(helper, CENTER); + } + + @GameTest(template = EMPTY) + public static void scriptReferencesAndCopies(GameTestHelper helper) { + ScriptHookSmoke.referencesAndCopies(helper, CENTER); + } + + @GameTest(template = EMPTY) + public static void scriptToolSettings(GameTestHelper helper) { + ScriptHookSmoke.toolSettings(helper); + } + @GameTest(template = EMPTY) public static void equippedContainer(GameTestHelper helper) { ContainerDrawerSmoke.check( diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ScriptHookSmoke.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ScriptHookSmoke.java index d91c004d..f1c1d715 100644 --- a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ScriptHookSmoke.java +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ScriptHookSmoke.java @@ -36,6 +36,101 @@ private static ServerPlayer player(GameTestHelper helper, ItemStack held) { return player; } + static void generatedLoot(GameTestHelper helper, BlockPos center) { + var level = helper.getLevel(); + var held = new ItemStack(net.minecraft.world.item.Items.DIAMOND_PICKAXE); + var player = player(helper, held); + var expected = java.util.Map.of( + "phase4_default", "mmdlib:phase4_default", + "phase4_self", "mmdlib:phase4_self", + "phase4_none", "minecraft:air", + "phase4_reference", "minecraft:cobblestone", + "phase4_override", "minecraft:diamond", + "phase4_itemless", "minecraft:air"); + int offset = 0; + for (var entry : expected.entrySet()) { + var pos = helper.absolutePos(center).offset(offset++ * 3, 0, 0); + var block = BuiltInRegistries.BLOCK.get(ResourceLocation.parse("mmdlib:" + entry.getKey())); + level.setBlock(pos, block.defaultBlockState(), 3); + require(player.gameMode.destroyBlock(pos), "player broke " + entry.getKey()); + var drops = level.getEntitiesOfClass(net.minecraft.world.entity.item.ItemEntity.class, + new net.minecraft.world.phys.AABB(pos).inflate(1)); + if (entry.getValue().equals("minecraft:air")) require(drops.isEmpty(), "expected no drops: " + entry.getKey()); + else require(drops.size() == 1 && drops.getFirst().getItem().getCount() == 1 + && drops.getFirst().getItem().is(BuiltInRegistries.ITEM.get(ResourceLocation.parse(entry.getValue()))), + "expected drop " + entry.getValue() + " for " + entry.getKey()); + drops.forEach(net.minecraft.world.entity.Entity::discard); + } + var item = stack("mmdlib:phase4_item"); + require(item.getMaxStackSize() == 16 && item.getItem().getCraftingRemainingItem() + == net.minecraft.world.item.Items.BUCKET, "fluent item aliases applied"); + helper.succeed(); + } + + static void referencesAndCopies(GameTestHelper helper, BlockPos center) { + var source = stack("mmdlib:phase3_source_item"); + var copy = stack("mmdlib:phase3_copy_item"); + var edited = stack("mmdlib:phase3_edited_item"); + require(source.getUseDuration(helper.makeMockServerPlayerInLevel()) == 20, + "source retains own configuration"); + require(copy.getUseDuration(helper.makeMockServerPlayerInLevel()) == 12, + "copy snapshots configuration before source edits"); + for (var entry : java.util.Map.of(source, 9, copy, 6, edited, 3).entrySet()) { + var player = player(helper, entry.getKey()); + player.gameMode.useItem(player, helper.getLevel(), entry.getKey(), InteractionHand.MAIN_HAND); + require(player.getUseItemRemainingTicks() == entry.getValue(), "copied handler map is independent"); + } + require(source.is(net.minecraft.tags.TagKey.create(net.minecraft.core.registries.Registries.ITEM, + ResourceLocation.parse("c:phase3_source"))), "same-run module tags inherited"); + require(copy.is(net.minecraft.tags.TagKey.create(net.minecraft.core.registries.Registries.ITEM, + ResourceLocation.parse("c:phase3_copy"))), "copy tags applied"); + require(!source.is(net.minecraft.tags.TagKey.create(net.minecraft.core.registries.Registries.ITEM, + ResourceLocation.parse("c:phase3_copy"))), "copy tag edits do not leak"); + require(stack("mmdlib:phase3_remainder").getItem().getCraftingRemainingItem() + == stack("mmdlib:phase3_later").getItem(), "forward item reference resolves at registration"); + int offset = 0; + for (var entry : java.util.Map.of("phase3_block", 8, "phase3_copy_block", 4).entrySet()) { + var pos = helper.absolutePos(center).offset(offset++, 0, 0); + var block = BuiltInRegistries.BLOCK.get(ResourceLocation.parse("mmdlib:" + entry.getKey())); + helper.getLevel().setBlock(pos, block.defaultBlockState(), 3); + var player = player(helper, ItemStack.EMPTY); + var hit = new BlockHitResult(Vec3.atCenterOf(pos), Direction.UP, pos, false); + player.gameMode.useItemOn(player, helper.getLevel(), ItemStack.EMPTY, InteractionHand.MAIN_HAND, hit); + require(helper.getLevel().getBlockState(pos).getValue(RedstoneBlock.OUTPUT) == entry.getValue(), + "block module callback snapshot"); + } + helper.succeed(); + } + + static void toolSettings(GameTestHelper helper) { + var stack = stack("mmdlib:phase2_phase2_tool"); + require(stack.getItem() instanceof zone.moddev.mc.lib.common.runtime.StandardItems.Tool, + "script configured generic tool registered"); + var tool = (zone.moddev.mc.lib.common.runtime.StandardItems.Tool) stack.getItem(); + var tier = tool.getTier(); + require(stack.getMaxDamage() == 200, "material tier durability (TieredItem overrides factory scaling)"); + require(tier.getSpeed() == 8 && tier.getAttackDamageBonus() == 3 + && tier.getEnchantmentValue() == 17, "named material stats"); + require(tier.getRepairIngredient().test(new ItemStack(net.minecraft.world.item.Items.DIAMOND)), + "explicit repair ingredient"); + require(tier.getIncorrectBlocksForDrops().location().toString() + .equals("minecraft:incorrect_for_wooden_tool"), "harvest restriction tag"); + require(stack.canPerformAction(net.neoforged.neoforge.common.ItemAbilities.PICKAXE_DIG), + "configured tool ability"); + require(tool.isValidRepairItem(stack, new ItemStack(net.minecraft.world.item.Items.DIAMOND)), + "tool accepts configured repair item"); + require(stack.getDestroySpeed(Blocks.STONE.defaultBlockState()) == 4, + "scaled mining speed"); + require(!stack.isCorrectToolForDrops(Blocks.IRON_ORE.defaultBlockState()), + "incorrect tag prevents harvesting iron ore"); + var runtime = tool.material().runtime(); + require(runtime.baseBlock().toString().equals("minecraft:stone") + && runtime.blockSetType().toString().equals("minecraft:oak") + && runtime.blockSound().orElseThrow().equals("wood") + && runtime.mapColor().orElseThrow().equals("gold"), "material runtime settings survive planning"); + helper.succeed(); + } + /** Right-click on air, use start (duration 10 cut to 3) and finish. */ static void item(GameTestHelper helper, BlockPos center) { var level = helper.getLevel(); diff --git a/src/runtimeTest/scripts/mmdlib_references.zs b/src/runtimeTest/scripts/mmdlib_references.zs new file mode 100644 index 00000000..e9cadbfd --- /dev/null +++ b/src/runtimeTest/scripts/mmdlib_references.zs @@ -0,0 +1,51 @@ +#loader mmdlib +import mods.mmdlib.ItemBuilder; +import mods.mmdlib.ItemModule; +import mods.mmdlib.BlockModule; +import mods.mmdlib.BlockBuilder; +import mods.mmdlib.ItemReference; +import mods.mmdlib.BlockReference; +import mods.mmdlib.ItemModuleReference; +import mods.mmdlib.BlockModuleReference; +import mods.mmdlib.BlockTagReference; +import mods.mmdlib.ItemUseContext; +import mods.mmdlib.ClickContext; + +var source = new ItemModule("phase3_source", new ItemModuleReference("mmdlib:ingot")); +source.stackSize = 1; +source.useDuration = 12; +source.addTags("c:phase3_source"); +source.onUseStart((ctx as ItemUseContext) => { ctx.duration = 6; }); +var copied = source.copy("phase3_copy"); +copied.removeTags("all").addTags("c:phase3_copy"); +var edited = copied.copy("phase3_edited"); +edited.useDuration = 15; +edited.onUseStart((ctx as ItemUseContext) => { ctx.duration = 3; }); +source.onUseStart((ctx as ItemUseContext) => { ctx.duration = 9; }); +source.useDuration = 20; +source.build(); +copied.build(); +edited.build(); +new ItemBuilder("phase3_source_item", source.reference).build(); +new ItemBuilder("phase3_copy_item", copied.reference).build(); +new ItemBuilder("phase3_edited_item", edited.reference).build(); + +// Store a reference before its target item is built. Resolution stays in the existing registrar. +var remainder = new ItemBuilder("phase3_remainder"); +remainder.remainder(new ItemReference("mmdlib:phase3_later")); +remainder.build(); +new ItemBuilder("phase3_later").build(); + +var blocks = new BlockModule("phase3_blocks", new BlockModuleReference("mmdlib:block")); +blocks.redstoneOutput = true; +blocks.onRightClick((ctx as ClickContext) => { ctx.output = 4; }); +var blockCopy = blocks.copy("phase3_blocks_copy"); +blocks.onRightClick((ctx as ClickContext) => { ctx.output = 8; }); +blockCopy.hardness = 5; +blockCopy.addTag(new BlockTagReference("minecraft:mineable/axe")); +blocks.build(); +blockCopy.build(); +new BlockBuilder("phase3_block", blocks.reference).build(); +var copiedBlock = new BlockBuilder("phase3_copy_block", blockCopy.reference); +copiedBlock.baseBlockFrom(new BlockReference("minecraft:stone")); +copiedBlock.build(); diff --git a/src/runtimeTest/scripts/mmdlib_resources.zs b/src/runtimeTest/scripts/mmdlib_resources.zs new file mode 100644 index 00000000..00fcfb7e --- /dev/null +++ b/src/runtimeTest/scripts/mmdlib_resources.zs @@ -0,0 +1,29 @@ +#loader mmdlib +import mods.mmdlib.BlockBuilder; +import mods.mmdlib.ItemBuilder; +import mods.mmdlib.ItemReference; + +var plain = new BlockBuilder("phase4_default"); +plain.displayName = "Default Named Block"; +plain.strength(2, 6).requiresToolToDrop(); +plain.dropsNormally(); +plain.build(); +var self = new BlockBuilder("phase4_self"); +self.destroyTime(2).explosionResistance(6).dropsSelf(); +self.build(); +var empty = new BlockBuilder("phase4_none"); +empty.noDrops(); +empty.build(); +var referenced = new BlockBuilder("phase4_reference"); +referenced.dropsFrom("minecraft:blocks/stone"); +referenced.build(); +var overridden = new BlockBuilder("phase4_override"); +overridden.noDrops(); +overridden.build(); +var itemless = new BlockBuilder("phase4_itemless"); +itemless.noCorrespondingItem().noCollisions().noOcclusion(); +itemless.build(); +var item = new ItemBuilder("phase4_item"); +item.stacksTo(16).craftRemainder(new ItemReference("minecraft:bucket")); +item.displayName = "Named Widget"; +item.build(); diff --git a/src/runtimeTest/scripts/mmdlib_tools.zs b/src/runtimeTest/scripts/mmdlib_tools.zs new file mode 100644 index 00000000..52c3b478 --- /dev/null +++ b/src/runtimeTest/scripts/mmdlib_tools.zs @@ -0,0 +1,29 @@ +#loader mmdlib +import mods.mmdlib.MaterialBuilder; +import mods.mmdlib.ItemModule; +import mods.mmdlib.ToolSettings; + +var module = new ItemModule("phase2_tool", "mmdlib:crackhammer"); +var settings = new ToolSettings(); +settings.mineable = "minecraft:mineable/pickaxe"; +settings.fastOn = "minecraft:mineable/pickaxe"; +settings.attackDamage = 4; +settings.attackSpeed = -2.5; +settings.abilities("pickaxe"); +settings.durability(0.5, 1, ""); +settings.speed(0.5, 1, ""); +module.configureTool(settings); +module.build(); + +var material = new MaterialBuilder("phase2"); +material.removeItems("all").addItems("phase2_tool"); +material.removeBlocks("all"); +material.hasFluid = false; +material.repairItem = "minecraft:diamond"; +material.incorrectBlocks = "minecraft:incorrect_for_wooden_tool"; +material.baseBlock = "minecraft:stone"; +material.blockSetType = "minecraft:oak"; +material.blockSound = "wood"; +material.mapColor = "gold"; +material.toolDurability(200).toolEfficiency(8).attackDamage(3).enchantability(17); +material.build(); diff --git a/src/runtimeTest/scripts/mmdlibdata/data/mmdlib/loot_table/blocks/phase4_override.json b/src/runtimeTest/scripts/mmdlibdata/data/mmdlib/loot_table/blocks/phase4_override.json new file mode 100644 index 00000000..63ed019f --- /dev/null +++ b/src/runtimeTest/scripts/mmdlibdata/data/mmdlib/loot_table/blocks/phase4_override.json @@ -0,0 +1,4 @@ +{ + "type": "minecraft:block", + "pools": [{"rolls": 1, "entries": [{"type": "minecraft:item", "name": "minecraft:diamond"}]}] +} diff --git a/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptFluentPropertiesTest.java b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptFluentPropertiesTest.java new file mode 100644 index 00000000..8e7df81f --- /dev/null +++ b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptFluentPropertiesTest.java @@ -0,0 +1,54 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import com.google.gson.JsonObject; +import com.mojang.serialization.JsonOps; +import net.minecraft.resources.ResourceLocation; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import zone.moddev.mc.lib.common.codecs.BlockTemplate; +import zone.moddev.mc.lib.common.codecs.ItemTemplate; +import zone.moddev.mc.lib.common.types.FactoryData; + +final class ScriptFluentPropertiesTest { + private static final class Block extends ScriptBlockProperties { + Block() { super(BlockTemplate.CODEC.parse(JsonOps.INSTANCE, new JsonObject()).getOrThrow(), FactoryData.EMPTY); } + ResourceLocation definitionId() { return ResourceLocation.parse("mmdlib:test"); } + ResourceLocation factoryId() { return ResourceLocation.parse("mmdlib:block"); } + public void build() { validateBlock(); template().toBuilder(); } + } + private static final class Item extends ScriptItemProperties { + Item() { super(ItemTemplate.CODEC.parse(JsonOps.INSTANCE, new JsonObject()).getOrThrow(), FactoryData.EMPTY, java.util.Set.of()); } + ResourceLocation definitionId() { return ResourceLocation.parse("mmdlib:test"); } + public void build() { template().toBuilder().setName("mmdlib:test").forMaterial(ResourceLocation.parse("mmdlib:test")).build(); } + } + + @Test + void fluentAndPropertyFormsProduceEquivalentDefinitionsAndErrors() { + var fluent = new Block(); var properties = new Block(); + fluent.strength(2, 6).noCollisions().noOcclusion().requiresToolToDrop().noCorrespondingItem(); + properties.setHardness(2); properties.setBlastResistance(6); properties.setCollision(false); + properties.setOcclusion(false); properties.setRequiresCorrectTool(true); properties.setHasItem(false); + assertEquals(properties.template(), fluent.template()); + fluent.destroyTime(-2); properties.setHardness(-2); + assertEquals(assertThrows(IllegalArgumentException.class, properties::build).getMessage(), + assertThrows(IllegalArgumentException.class, fluent::build).getMessage()); + var a = new Item(); var b = new Item(); + a.stacksTo(16).craftRemainder(new ScriptItemReference("minecraft:bucket")); + b.setStackSize(16); b.setContainer("minecraft:bucket"); + assertEquals(b.template(), a.template()); + a.stacksTo(0); b.setStackSize(0); + assertEquals(assertThrows(IllegalArgumentException.class, b::build).getMessage(), + assertThrows(IllegalArgumentException.class, a::build).getMessage()); + } + + @Test + void rejectsAmbiguousOrInvalidResourceRequests() { + var block = new Block(); + assertThrows(IllegalArgumentException.class, () -> block.setDisplayName(" ")); + assertThrows(IllegalArgumentException.class, () -> block.dropsFrom("mmdlib:blocks/test")); + block.noCorrespondingItem().dropsSelf(); + assertThrows(IllegalArgumentException.class, block::build); + block.noDrops(); + assertDoesNotThrow(block::build); + } +} diff --git a/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java index 0c2e518a..f3d5f301 100644 --- a/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java +++ b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java @@ -23,6 +23,28 @@ final class ScriptMaterialResourcesTest { private static final Path TEMPLATES = Path.of("src/main/resources/mmdlib_templates"); + @Test + void namesAndLootOptionsUseExistingGeneratedPackPaths() throws Exception { + var result = everything(); + var options = Map.of( + "material|mmdlib:cold_iron", ScriptResourceOptions.DEFAULT.named("Bright Alloy"), + "item_module|mmdlib:ingot", ScriptResourceOptions.DEFAULT.named("__material__ Bar"), + "block_module|mmdlib:block", ScriptResourceOptions.DEFAULT.drops("none", "")); + var assets = ScriptMaterialResources.assets(List.of(result), List.of(), List.of(), Map.of(), + new MaterialTemplates(List.of(TEMPLATES)), TEMPLATES, options); + var lang = JsonParser.parseString(new String(assets.get("assets/mmdlib/lang/en_us.json"), StandardCharsets.UTF_8)).getAsJsonObject(); + org.junit.jupiter.api.Assertions.assertEquals("Bright Alloy Bar", lang.get("item.mmdlib.cold_iron_ingot").getAsString()); + var data = ScriptMaterialResources.data(List.of(result), List.of(), List.of(), Map.of(), options); + var path = "data/mmdlib/loot_table/blocks/cold_iron_block.json"; + var empty = JsonParser.parseString(new String(data.get(path), StandardCharsets.UTF_8)).getAsJsonObject(); + org.junit.jupiter.api.Assertions.assertEquals(0, empty.getAsJsonArray("pools").size()); + var delegated = ScriptMaterialResources.data(List.of(result), List.of(), List.of(), Map.of(), + Map.of("block_module|mmdlib:block", ScriptResourceOptions.DEFAULT.drops("table", "minecraft:blocks/stone"))); + var table = JsonParser.parseString(new String(delegated.get(path), StandardCharsets.UTF_8)).getAsJsonObject(); + org.junit.jupiter.api.Assertions.assertEquals("minecraft:blocks/stone", table.getAsJsonArray("pools") + .get(0).getAsJsonObject().getAsJsonArray("entries").get(0).getAsJsonObject().get("value").getAsString()); + } + private static MaterialFactory.Result everything() { var material = MaterialBuilder.create() diff --git a/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptReferencesTest.java b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptReferencesTest.java new file mode 100644 index 00000000..4a5f5d9e --- /dev/null +++ b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptReferencesTest.java @@ -0,0 +1,26 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +final class ScriptReferencesTest { + @Test + void referencesAcceptUnregisteredIdsWithoutLookingUpRegistries() { + String id = "uninstalled_mod:future_content"; + assertEquals(id, new ScriptItemReference(id).getId()); + assertEquals(id, new ScriptBlockReference(id).getId()); + assertEquals(id, new ScriptBlockTagReference(id).getId()); + assertEquals(id, new ScriptItemModuleReference(id).getId()); + assertEquals(id, new ScriptBlockModuleReference(id).getId()); + } + + @Test + void malformedIdsFailAtReferenceConstruction() { + assertThrows(net.minecraft.ResourceLocationException.class, () -> new ScriptItemReference("Invalid Name")); + assertThrows(net.minecraft.ResourceLocationException.class, () -> new ScriptBlockReference("Invalid Name")); + assertThrows(net.minecraft.ResourceLocationException.class, () -> new ScriptBlockTagReference("#minecraft:stone")); + assertThrows(net.minecraft.ResourceLocationException.class, () -> new ScriptItemModuleReference("Invalid Name")); + assertThrows(net.minecraft.ResourceLocationException.class, () -> new ScriptBlockModuleReference("Invalid Name")); + } + +} diff --git a/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptToolSettingsTest.java b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptToolSettingsTest.java new file mode 100644 index 00000000..50a0bf91 --- /dev/null +++ b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptToolSettingsTest.java @@ -0,0 +1,35 @@ +package zone.moddev.mc.lib.compat.CraftTweaker; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import zone.moddev.mc.lib.common.types.FactoryData; +import zone.moddev.mc.lib.common.runtime.StandardItems; + +final class ScriptToolSettingsTest { + @Test + void typedSettingsDecodeThroughRuntimeCodecAndSnapshotIndependently() { + var settings = new ScriptToolSettings(); + settings.setMineable("minecraft:mineable/pickaxe"); + settings.setAttackDamage(4); + settings.abilities("pickaxe").durability(0.5, 1, "").speed(0.25, 1, "custom_speed"); + var snapshot = settings.json(); + settings.setAttackDamage(9); + var decoded = new FactoryData(snapshot).decode(StandardItems.ToolSettings.CODEC).getOrThrow(); + assertEquals(4, decoded.attackDamage()); + assertEquals(0.5, decoded.durability().toolFactor()); + assertTrue(decoded.durability().stat().isEmpty()); + assertEquals("custom_speed", decoded.speed().stat().orElseThrow()); + assertFalse(decoded.abilities().isEmpty()); + assertFalse(snapshot.has("fast_on"), "omitted keys must preserve module configuration"); + } + + @Test + void rejectsInvalidScalesAndNonfiniteAttributesBeforeWriting() { + var settings = new ScriptToolSettings(); + assertThrows(IllegalArgumentException.class, () -> settings.speed(-1, 0, "")); + assertThrows(IllegalArgumentException.class, () -> settings.durability(1, 0, "")); + assertThrows(IllegalArgumentException.class, () -> settings.setAttackSpeed(Float.NaN)); + assertThrows(IllegalArgumentException.class, () -> settings.abilities(" ")); + assertTrue(settings.json().entrySet().isEmpty()); + } +} From a474b4738844573eca6e97d24bc269a6f9ae7344 Mon Sep 17 00:00:00 2001 From: Daniel Hazelton Date: Sat, 7 Nov 2026 15:37:13 -0500 Subject: [PATCH 6/9] Fix code review findings in the CraftTweaker integration - Copying a block module keeps a constant redstone output, and writes a constant comparator level back as a number instead of a string that RedstoneBlock rejects. - item-hit fires only for player and mob attack damage; thorns and exploding creepers also name one entity as direct and causing source. - Definition source names must be unique; a second source with the same name used to replace the first one's definitions silently. - Plain items with a food component keep vanilla's use, so the hunger check still applies when a use time is set. - Script hooks check that building is still allowed before changing anything, so a late call cannot swap a live handler. - Duplicate standalone IDs from two sources no longer blame a definition file. - Work out whether a material has a slab once, not once per block. Co-Authored-By: Claude Opus 5.5 --- .../zone/moddev/mc/lib/api/MaterialExtensions.java | 10 +++++++--- .../mc/lib/common/loading/DefinitionLoader.java | 7 ++++--- .../mc/lib/common/runtime/ItemEventBridge.java | 12 +++++++++--- .../moddev/mc/lib/common/runtime/StandardItems.java | 7 ++++++- .../compat/CraftTweaker/ScriptBlockProperties.java | 8 +++++++- .../CraftTweaker/ScriptDefinitionProperties.java | 4 +++- .../compat/CraftTweaker/ScriptMaterialResources.java | 3 +-- 7 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java b/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java index b2f4a3e2..93b16ee0 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java +++ b/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java @@ -457,11 +457,15 @@ public MaterialEvents events() { * same checks as definition files. Content outside MMDLib's namespace is only registered by its * own mod's {@link zone.moddev.mc.lib.common.runtime.RuntimeContentRegistrar}. * - * @param name names the source in loading errors + * @param name names the source in loading errors; unique among sources * @param source supplies the definitions + * @throws IllegalArgumentException if a source with this name is already registered */ - public void registerDefinitionSource(String name, DefinitionSource source) { - definitionSources.add(new NamedSource(name, source)); + public synchronized void registerDefinitionSource(String name, DefinitionSource source) { + var named = new NamedSource(name, source); + if (definitionSources.stream().anyMatch(existing -> existing.name().equals(name))) + throw new IllegalArgumentException("Duplicate definition source '" + name + "'"); + definitionSources.add(named); } /** The registered definition sources, in registration order. */ diff --git a/src/main/java/zone/moddev/mc/lib/common/loading/DefinitionLoader.java b/src/main/java/zone/moddev/mc/lib/common/loading/DefinitionLoader.java index eb217c31..a5820641 100644 --- a/src/main/java/zone/moddev/mc/lib/common/loading/DefinitionLoader.java +++ b/src/main/java/zone/moddev/mc/lib/common/loading/DefinitionLoader.java @@ -156,7 +156,8 @@ public static Loaded load( var sourced = new LinkedHashMap(); for (var source : api.definitionSources()) { var origin = "definition source '" + source.name() + "'"; - sourced.put(origin, Objects.requireNonNull(source.source().load(api), origin)); + if (sourced.put(origin, Objects.requireNonNull(source.source().load(api), origin)) != null) + throw new IllegalArgumentException("Duplicate " + origin); } var factory = new MaterialFactory(api, installed); var materials = new TreeMap(); @@ -221,7 +222,7 @@ public static Loaded load( (id, definition) -> { if (standalone.containsKey(id)) throw new IllegalArgumentException( - "Duplicate standalone block " + id + ": a definition file and " + origin); + "Duplicate standalone block " + id + ": " + origin + " and an earlier definition"); try { var module = api.blockModule(definition.module()); if (module.requiredMod().isPresent() @@ -265,7 +266,7 @@ public static Loaded load( (id, definition) -> { if (standaloneItems.containsKey(id)) throw new IllegalArgumentException( - "Duplicate standalone item " + id + ": a definition file and " + origin); + "Duplicate standalone item " + id + ": " + origin + " and an earlier definition"); try { var module = api.itemModule(definition.module()); if (module.requiredMod().isPresent() diff --git a/src/main/java/zone/moddev/mc/lib/common/runtime/ItemEventBridge.java b/src/main/java/zone/moddev/mc/lib/common/runtime/ItemEventBridge.java index e52e0e44..a8abcc1a 100644 --- a/src/main/java/zone/moddev/mc/lib/common/runtime/ItemEventBridge.java +++ b/src/main/java/zone/moddev/mc/lib/common/runtime/ItemEventBridge.java @@ -137,15 +137,21 @@ private ItemUse use(LivingEntityUseItemEvent event, UsePhase phase, MaterialEven } /** - * Server only, after damage was dealt. A melee hit is damage whose direct source is the attacker - * itself, which excludes projectiles, thorns and the like; the attacker's main-hand item is bound. + * Server only, after damage was dealt. A melee hit is player or mob attack damage dealt directly + * by the attacker, which excludes projectiles, thorns, explosions and the like (thorns and an + * exploding creeper also name the entity as both direct and causing source); the attacker's + * main-hand item is bound. */ private void meleeHit(LivingDamageEvent.Post event) { var target = event.getEntity(); var source = event.getSource(); if (target.level().isClientSide || !(source.getEntity() instanceof LivingEntity attacker) - || source.getDirectEntity() != attacker) return; + || source.getDirectEntity() != attacker + || !(source.is(net.minecraft.world.damagesource.DamageTypes.PLAYER_ATTACK) + || source.is(net.minecraft.world.damagesource.DamageTypes.MOB_ATTACK) + || source.is(net.minecraft.world.damagesource.DamageTypes.MOB_ATTACK_NO_AGGRO))) + return; var stack = attacker.getMainHandItem(); dispatch( stack, MaterialEventTypes.ITEM_HIT, new ItemHit(attacker, target, stack, event.getNewDamage())); diff --git a/src/main/java/zone/moddev/mc/lib/common/runtime/StandardItems.java b/src/main/java/zone/moddev/mc/lib/common/runtime/StandardItems.java index ef6845e5..02e188f1 100644 --- a/src/main/java/zone/moddev/mc/lib/common/runtime/StandardItems.java +++ b/src/main/java/zone/moddev/mc/lib/common/runtime/StandardItems.java @@ -354,7 +354,12 @@ public net.minecraft.world.InteractionResultHolder use( net.minecraft.world.level.Level level, net.minecraft.world.entity.player.Player player, net.minecraft.world.InteractionHand hand) { - if (useDuration <= 0) return super.use(level, player, hand); + // Food keeps vanilla's use, which refuses to start eating when the player cannot eat. + if (useDuration <= 0 + || player + .getItemInHand(hand) + .has(net.minecraft.core.component.DataComponents.FOOD)) + return super.use(level, player, hand); player.startUsingItem(hand); return net.minecraft.world.InteractionResultHolder.consume(player.getItemInHand(hand)); } diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java index 48b83618..d056207f 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java @@ -53,6 +53,8 @@ public abstract class ScriptBlockProperties extends ScriptDefinitionProperties { private boolean directional; private int redstoneDelay = 1; private String comparator = ""; + // A constant 0-15 output copied from the base module; scripts only set "state" output. + private Integer constantOutput; private Set outputFaces; private Set inputFaces; private boolean classHooks; @@ -65,6 +67,7 @@ public abstract class ScriptBlockProperties extends ScriptDefinitionProperties { redstone = true; redstoneInput = r.has("input") && r.get("input").getAsBoolean(); redstoneOutput = r.has("output") && r.get("output").getAsString().equals("state"); + if (r.has("output") && !redstoneOutput) constantOutput = r.get("output").getAsInt(); strongPower = r.has("strong") && r.get("strong").getAsBoolean(); directional = r.has("facing") && r.get("facing").getAsBoolean(); redstoneDelay = r.has("delay") ? r.get("delay").getAsInt() : 0; @@ -548,10 +551,13 @@ FactoryData factoryData() { var settings = new JsonObject(); if (redstoneInput) settings.addProperty("input", true); if (redstoneOutput) settings.addProperty("output", "state"); + else if (constantOutput != null) settings.addProperty("output", constantOutput); if (strongPower) settings.addProperty("strong", true); if (directional) settings.addProperty("facing", true); if (redstoneInput && redstoneDelay > 0) settings.addProperty("delay", redstoneDelay); - if (!comparator.isEmpty()) settings.addProperty("comparator", comparator); + // A constant comparator level must stay a number: RedstoneBlock reads strings as property names. + if (comparator.matches("\\d+")) settings.addProperty("comparator", Integer.parseInt(comparator)); + else if (!comparator.isEmpty()) settings.addProperty("comparator", comparator); if (outputFaces != null) settings.add("faces", array(outputFaces)); if (inputFaces != null && redstoneInput) settings.add("inputs", array(inputFaces)); json.add("redstone", settings); diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java index d223859a..5059797f 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java @@ -69,6 +69,8 @@ void hook( zone.moddev.mc.lib.api.MaterialEvent event, H handler, java.util.function.BiConsumer invoke) { + // Checked before any change, so a handler cannot be swapped on a live definition after startup. + var catalog = ScriptDefinitions.catalog(); var id = definitionId().withSuffix("/" + event.name()); boolean first = !hooks.containsKey(event.name()); hooks.put(event.name(), java.util.Objects.requireNonNull(handler)); @@ -77,7 +79,7 @@ void hook( target.hook(event, snapshot, invoke); }); if (first) - ScriptDefinitions.catalog() + catalog .events() .registerCallback( id, diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java index 0811604b..526971f3 100644 --- a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java @@ -328,6 +328,7 @@ static SortedMap data( var hardness = properties.hardness(); var level = hardness >= 10 ? "diamond" : hardness >= 8 ? "iron" : hardness >= 5 ? "stone" : null; + var hasSlab = plan.blocks().keySet().stream().anyMatch(other -> other.id().equals("slab")); plan.blocks() .forEach( (part, block) -> { @@ -336,8 +337,6 @@ static SortedMap data( properties.harvestTags().forEach(tag -> tag(tags, "block", tag.toString(), id)); tag(tags, "block", "minecraft:mineable/pickaxe", id); if (level != null) tag(tags, "block", "minecraft:needs_" + level + "_tool", id); - var hasSlab = - plan.blocks().keySet().stream().anyMatch(other -> other.id().equals("slab")); lootTable(part.factory(), id, NS + ":" + name + "_slab", hasSlab) .ifPresent( table -> From c60469f88c8412638b983c978adb04b6c648c23a Mon Sep 17 00:00:00 2001 From: Daniel Hazelton Date: Sat, 7 Nov 2026 15:52:09 -0500 Subject: [PATCH 7/9] Share the redstone signal helper and document face naming RedstoneBlock.signal now delegates to RedstoneChange.signal in the api package, so the face-signal rule exists once and api still does not depend on runtime code. docs/crafttweaker.md explains that on directional blocks compass names in addInputFaces/addOutputFaces are the block's own sides (as if facing north), while ctx.input() takes world directions, and recommends front, back, left and right, which mean the same in both. Co-Authored-By: Claude Opus 5.5 --- docs/crafttweaker.md | 11 +++++++++++ .../zone/moddev/mc/lib/api/MaterialEventContexts.java | 11 ++++++++++- .../moddev/mc/lib/common/runtime/RedstoneBlock.java | 4 ++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/crafttweaker.md b/docs/crafttweaker.md index 05ac3dc0..c3fe6cb1 100644 --- a/docs/crafttweaker.md +++ b/docs/crafttweaker.md @@ -468,6 +468,17 @@ and run script functions. These properties write the block's side, and `remove...("all")` clears them. A block that reads only some sides runs its handler when any of them changes. +On a directional block, compass names mean different things in the two places: + +* In `addInputFaces` and `addOutputFaces`, they are part of the block's definition, + so they name the block's own sides as if it faced north. The placed block turns + them with itself. +* In `ctx.input(side)`, the handler is running in the world, so they are world + directions. + +`front`, `back`, `left` and `right` mean the same in both places; use them on +directional blocks. + Two handlers take script functions: * `onRedstoneChanged((ctx as RedstoneContext) => {...})` runs on the server when the diff --git a/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java b/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java index 04d8d1fd..e1142c59 100644 --- a/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java +++ b/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java @@ -128,7 +128,16 @@ public boolean powered() { /** The power arriving at the block's {@code face} (a world direction) right now. */ public int signal(net.minecraft.core.Direction face) { - return level.getSignal(position.relative(face), face); + return signal(level, position, face); + } + + /** + * The power arriving at {@code face} (a world direction) of the block at {@code pos}, as {@link + * net.minecraft.world.level.Level#getSignal} reports it from that neighbor. + */ + public static int signal( + net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.core.Direction face) { + return level.getSignal(pos.relative(face), face); } } diff --git a/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java b/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java index d5fa42f4..72cdd2db 100644 --- a/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java +++ b/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java @@ -390,9 +390,9 @@ protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSou if (settings.input()) checkInput(state, level, pos); } - /** The power a face receives from its neighbor, as {@link Level#getSignal} reports it. */ + /** The power a face receives from its neighbor; see {@link RedstoneChange#signal(Level, BlockPos, Direction)}. */ public static int signal(Level level, BlockPos pos, Direction face) { - return level.getSignal(pos.relative(face), face); + return RedstoneChange.signal(level, pos, face); } private void checkInput(BlockState state, ServerLevel server, BlockPos pos) { From 7fb5e7879623aa021088772b4fdd39d5c23c0b50 Mon Sep 17 00:00:00 2001 From: Daniel Hazelton Date: Sat, 7 Nov 2026 15:54:50 -0500 Subject: [PATCH 8/9] Fire redstone-changed only when a read face's power changes A redstone block that reads only some faces dispatched redstone-changed on every neighbor update, including changes on faces it does not read. With no delay, a handler that toggled the block's output could loop until the stack overflowed. RedstoneBlock now remembers the last power on each read face, per position and level, outside block state (six faces of 0-15 do not fit in it). The event fires only when one of those faces changes, plus once when the block has no record yet: when placed, or on its first update after loading. Entries go when the block is removed, and a level's entries go with the level. The NAND gate game test now checks that powering an unread side does not run the handler. Co-Authored-By: Claude Opus 5.5 --- docs/runtime-behaviors.md | 2 +- .../mc/lib/common/runtime/RedstoneBlock.java | 39 ++++++++++++++++--- .../moddev/mc/lib/smoke/RedstoneSmoke.java | 11 ++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/docs/runtime-behaviors.md b/docs/runtime-behaviors.md index 9fdd10f4..ed9991ad 100644 --- a/docs/runtime-behaviors.md +++ b/docs/runtime-behaviors.md @@ -213,7 +213,7 @@ Only the block-state properties its settings use are added. | `faces` | list of `down`, `up`, `north`, `south`, `west`, `east` | Faces that emit (default all) | | `comparator` | `0`-`15`, `"input"`, or `"output"` | Comparator reading; `"input"` needs `input`, `"output"` needs `output: "state"` | | `facing` | `true` / `false` | The block faces the way its placer looked (a `facing` property, turned by structure rotation); `faces` and `inputs` may then name `front`, `back`, `left`, `right` | -| `inputs` | list of faces | Faces read (default all); needs `input`. Reading only some fires `redstone-changed` whenever any of them changes, even if the strongest stays the same | +| `inputs` | list of faces | Faces read (default all); needs `input`. Reading only some fires `redstone-changed` whenever one of them changes, even if the strongest stays the same, and once when placed (or on the first update after loading); changes on other faces are ignored | | `delay` | `0`-`100` | Ticks between a neighbour change and reading the inputs (default 0, at once); needs `input` | A single-block gate reads its sides and powers its front: diff --git a/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java b/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java index 72cdd2db..fbf86df9 100644 --- a/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java +++ b/src/main/java/zone/moddev/mc/lib/common/runtime/RedstoneBlock.java @@ -1,6 +1,12 @@ package zone.moddev.mc.lib.common.runtime; +import java.util.Arrays; import java.util.EnumSet; +import java.util.Map; +import java.util.WeakHashMap; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import java.util.Optional; import java.util.Set; @@ -101,8 +107,9 @@ public Settings( } /** - * Whether {@code redstone-changed} fires on any read face's change rather than only when the - * strongest input changes: true when only some faces are read. + * Whether {@code redstone-changed} fires when any read face's power changes rather than only + * when the strongest input changes: true when only some faces are read. Such a block also fires + * once when placed, or on its first update after loading. */ public boolean perFace() { return inputs.size() < Direction.values().length; @@ -230,6 +237,13 @@ private static IllegalArgumentException error(MaterialPart part, String message) } } + /** + * The last power on each read face (by local face ordinal) of blocks that read single faces, by + * position, per level. Block state has no room for six levels of 0-15. Server thread only; an + * entry goes when its block is removed, and a level's entries go with the level. + */ + private static final Map> FACE_POWER = new WeakHashMap<>(); + // createBlockStateDefinition runs inside Block's constructor, before fields are set. private static final ThreadLocal CONSTRUCTING = new ThreadLocal<>(); private final Settings settings; @@ -365,6 +379,10 @@ protected void onPlace( protected void onRemove( BlockState state, Level level, BlockPos pos, BlockState replacement, boolean moved) { boolean emitting = !moved && !state.is(replacement.getBlock()) && settings.emits(); + if (!state.is(replacement.getBlock()) && level instanceof ServerLevel server) { + var known = FACE_POWER.get(server); + if (known != null) known.remove(pos.asLong()); + } super.onRemove(state, level, pos, replacement, moved); if (emitting) updateNeighbours(level, pos, state); } @@ -397,10 +415,19 @@ public static int signal(Level level, BlockPos pos, Direction face) { private void checkInput(BlockState state, ServerLevel server, BlockPos pos) { int previous = state.getValue(INPUT), power = 0; - for (Direction face : settings.inputs()) - power = Math.max(power, signal(server, pos, toWorld(state, face))); - // Blocks reading single faces react to any face's change; the strongest may stay the same. - if (previous == power && !settings.perFace()) return; + var faces = new int[Direction.values().length]; + for (Direction face : settings.inputs()) { + int signal = signal(server, pos, toWorld(state, face)); + faces[face.ordinal()] = signal; + power = Math.max(power, signal); + } + if (settings.perFace()) { + // A block reading single faces reacts when any of them changes, even if the strongest stays + // the same; with no record (just placed, or loaded since) it reacts once to start. + var known = FACE_POWER.computeIfAbsent(server, level -> new Long2ObjectOpenHashMap<>()); + int[] last = known.put(pos.asLong(), faces); + if (last != null && Arrays.equals(last, faces)) return; + } else if (previous == power) return; BlockState updated = state.setValue(INPUT, power); if (previous != power) server.setBlock(pos, updated, Block.UPDATE_CLIENTS); var handler = diff --git a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/RedstoneSmoke.java b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/RedstoneSmoke.java index 4ae59227..6bcd2ea8 100644 --- a/src/runtimeTest/java/zone/moddev/mc/lib/smoke/RedstoneSmoke.java +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/RedstoneSmoke.java @@ -18,8 +18,12 @@ public final class RedstoneSmoke { private RedstoneSmoke() {} + private static final java.util.concurrent.atomic.AtomicInteger calls = + new java.util.concurrent.atomic.AtomicInteger(); + /** The gate's {@code redstone-changed} handler: off only while both sides are powered. */ public static void nand(RedstoneChange change) { + calls.incrementAndGet(); var block = (RedstoneBlock) change.state().getBlock(); boolean left = change.signal(block.toWorld(change.state(), Direction.WEST)) > 0; boolean right = change.signal(block.toWorld(change.state(), Direction.EAST)) > 0; @@ -37,11 +41,18 @@ static void gate(GameTestHelper helper, BlockPos center) { var left = pos.north(); var right = pos.south(); var back = pos.west(); + var mark = new int[1]; helper .startSequence() .thenExecuteAfter(3, () -> expect(helper, pos, 15, "unpowered gate is on")) + .thenExecute(() -> mark[0] = calls.get()) .thenExecute(() -> level.setBlock(back, Blocks.REDSTONE_BLOCK.defaultBlockState(), 3)) .thenExecuteAfter(3, () -> expect(helper, pos, 15, "the back side is not read")) + .thenExecute( + () -> { + if (calls.get() != mark[0]) + throw new AssertionError("an unread side's change ran the handler"); + }) .thenExecute(() -> level.setBlock(left, Blocks.REDSTONE_BLOCK.defaultBlockState(), 3)) .thenExecuteAfter(3, () -> expect(helper, pos, 15, "one input keeps it on")) .thenExecute(() -> level.setBlock(right, Blocks.REDSTONE_BLOCK.defaultBlockState(), 3)) From d796ee6b1fd20706da073218e3003b82c4e2fb76 Mon Sep 17 00:00:00 2001 From: Daniel Hazelton Date: Sat, 7 Nov 2026 16:42:00 -0500 Subject: [PATCH 9/9] Keep CraftTweaker out of smoke clients and fix the smoke deadline The companion client smoke failed in CI: its 240-second deadline started when the test class loaded, so it counted launch time, and loading CraftTweaker into that client made world creation about 28 seconds slower. Locally it already passed with only about 18 seconds to spare. - CompanionClientSmoke gets a 6-minute budget that starts at the title screen. Menus that never render still fail after 400 ticks. - CraftTweaker's dev-run dependency moves to its own configuration, craftTweakerDevRuntime. The companion runs (client smoke, game tests, companion client) and the GUI smoke clients leave it and its ZenCode libraries out; they don't test it, and the CraftTweaker game tests do. runClient and runServer still load it. Co-Authored-By: Claude Opus 5.5 --- build.gradle | 6 +++++- scripts/companion.gradle | 8 ++++++-- scripts/gui-smoke.init.gradle | 2 ++ .../mc/companion/test/CompanionClientSmoke.java | 12 ++++++++++-- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 59cb40dd..4e09b8b0 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,10 @@ repositories { configurations { localRuntime runtimeClasspath.extendsFrom localRuntime + // CraftTweaker for dev runs, in its own configuration so the machine (companion) and GUI smoke + // runs can leave it out: they don't test it, and it slows their clients' world loading. + craftTweakerDevRuntime + localRuntime.extendsFrom craftTweakerDevRuntime // CraftTweaker's processor path brings slf4j-api 1.8 betas, whose ServiceLoader lookup finds // Gradle's own provider from another classloader and fails; 1.7.x binds statically instead. annotationProcessor { @@ -63,7 +67,7 @@ dependencies { annotationProcessor "com.blamejared.crafttweaker:CraftTweaker-neoforge-1.21.1:${crafttweaker_version}" // Dev runs include CraftTweaker; pass -PwithoutCraftTweaker to launch without it. if (!providers.gradleProperty('withoutCraftTweaker').isPresent()) { - localRuntime "com.blamejared.crafttweaker:CraftTweaker-neoforge-1.21.1:${crafttweaker_version}" + craftTweakerDevRuntime "com.blamejared.crafttweaker:CraftTweaker-neoforge-1.21.1:${crafttweaker_version}" } } diff --git a/scripts/companion.gradle b/scripts/companion.gradle index bcd58c75..4d7d2d10 100644 --- a/scripts/companion.gradle +++ b/scripts/companion.gradle @@ -1,7 +1,11 @@ +// The companion's machine runs don't test CraftTweaker (the library's game tests do), so they run +// without it and its ZenCode libraries. +def mainRuntimeWithoutCraftTweaker = sourceSets.main.runtimeClasspath.minus(configurations.craftTweakerDevRuntime) + sourceSets { companion { compileClasspath += sourceSets.main.output + sourceSets.main.compileClasspath - runtimeClasspath += sourceSets.main.output + sourceSets.main.runtimeClasspath + runtimeClasspath += sourceSets.main.output + mainRuntimeWithoutCraftTweaker } companionTest { compileClasspath += sourceSets.companion.output + sourceSets.companion.compileClasspath @@ -10,7 +14,7 @@ sourceSets { // Mock GameTest connections cannot negotiate Curios/Jade login-sync payloads. // Reuse the compiled fixtures, but keep optional mods for real client/server smoke runs. companionServerTest { - runtimeClasspath += sourceSets.companionTest.output + sourceSets.companion.output + sourceSets.main.output + sourceSets.main.runtimeClasspath + runtimeClasspath += sourceSets.companionTest.output + sourceSets.companion.output + sourceSets.main.output + mainRuntimeWithoutCraftTweaker } } diff --git a/scripts/gui-smoke.init.gradle b/scripts/gui-smoke.init.gradle index d2963800..63f04bc1 100644 --- a/scripts/gui-smoke.init.gradle +++ b/scripts/gui-smoke.init.gradle @@ -4,6 +4,8 @@ allprojects { afterEvaluate { if (extensions.findByName('neoForge') != null) { + // The GUI smoke doesn't test CraftTweaker (the game tests do); keep it out of this client. + configurations.named('craftTweakerDevRuntime') { dependencies.clear() } sourceSets.main.java.srcDir(file('src/runtimeTest/java')) sourceSets.main.resources.srcDir(file('src/runtimeTest/resources')) def smokeDir = file(providers.gradleProperty('withoutJei').isPresent() ? 'build/gui-smoke-without-jei' : 'build/gui-smoke') diff --git a/src/companionTest/java/zone/moddev/mc/companion/test/CompanionClientSmoke.java b/src/companionTest/java/zone/moddev/mc/companion/test/CompanionClientSmoke.java index a4f2ab84..1a92211b 100644 --- a/src/companionTest/java/zone/moddev/mc/companion/test/CompanionClientSmoke.java +++ b/src/companionTest/java/zone/moddev/mc/companion/test/CompanionClientSmoke.java @@ -35,7 +35,14 @@ @EventBusSubscriber(modid = CompanionMod.ID, value = Dist.CLIENT) public final class CompanionClientSmoke { private static final BlockPos POS = new BlockPos(0, 100, 0); - private static final long DEADLINE = System.currentTimeMillis() + 240_000; + /** + * Time the whole smoke may take once the title screen is up. Launch time is not counted: it varies + * with the machine, and a slow CI runner with software rendering needs most of this budget for the + * menus alone. A menu that never renders still fails within 400 ticks (see {@link #tick}). + */ + private static final long BUDGET_MILLIS = 360_000; + + private static long deadline = Long.MAX_VALUE; private static boolean started, opening, menuRequested, finished; private static int stage, frames, waitingTicks, drawerStage, fluidStage, controlStage; private static boolean controlFixtureRequested; @@ -58,9 +65,10 @@ public static void tick(ClientTickEvent.Post event) { if (!Boolean.getBoolean("mmdlib.companionSmoke") || finished) return; var mc = Minecraft.getInstance(); try { - if (System.currentTimeMillis() > DEADLINE) + if (System.currentTimeMillis() > deadline) throw new AssertionError("Timed out on " + mc.screen); if (!started && mc.screen instanceof TitleScreen) { + deadline = System.currentTimeMillis() + BUDGET_MILLIS; if (!net.neoforged.fml.ModList.get().isLoaded("jade")) throw new AssertionError("Jade must be installed in companion runs"); MMDLib.LOGGER.info("JADE_COMPANION_PRESENT");