diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..60f805b1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,37 @@ +# 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). + ++- 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/build.gradle b/build.gradle index 406c15a9..4e09b8b0 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,15 @@ 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 { + resolutionStrategy.force 'org.slf4j:slf4j-api:1.7.36' + } } dependencies { @@ -51,6 +60,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()) { + craftTweakerDevRuntime "com.blamejared.crafttweaker:CraftTweaker-neoforge-1.21.1:${crafttweaker_version}" + } } neoForge { @@ -109,6 +127,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-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 new file mode 100644 index 00000000..c3fe6cb1 --- /dev/null +++ b/docs/crafttweaker.md @@ -0,0 +1,730 @@ +# CraftTweaker definitions + +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. + +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. + +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 +#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. + +### 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, +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 | +| `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 | +| `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. + +## 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, +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. + +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 + 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 +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-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. +* 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 + +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). + +## 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/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/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/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/docs/runtime-behaviors.md b/docs/runtime-behaviors.md index 25da7cb8..ed9991ad 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 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: + +```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.properties b/gradle.properties index bee48734..0c894216 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 @@ -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/gradle/verification.gradle b/gradle/verification.gradle index b2252834..c3be6bfe 100644 --- a/gradle/verification.gradle +++ b/gradle/verification.gradle @@ -38,12 +38,26 @@ 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`). 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/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/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/scripts/generate_material_templates.py b/scripts/generate_material_templates.py new file mode 100644 index 00000000..6b50f437 --- /dev/null +++ b/scripts/generate_material_templates.py @@ -0,0 +1,418 @@ +#!/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}) + # 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', + {'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/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/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 00000000..fa14972f Binary files /dev/null and b/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate.png differ 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 00000000..a529b6c4 Binary files /dev/null and b/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate_front.png differ diff --git a/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate_top.png b/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate_top.png new file mode 100644 index 00000000..9cc7fa4b Binary files /dev/null and b/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/block/client_gate_top.png differ diff --git a/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/item/client_widget.png b/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/item/client_widget.png new file mode 100644 index 00000000..9beb8459 Binary files /dev/null and b/src/clientSmoke/scripts/mmdlibdata/assets/mmdlib/textures/item/client_widget.png differ 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"); 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/MaterialEventContexts.java b/src/main/java/zone/moddev/mc/lib/api/MaterialEventContexts.java index ec18c8e6..e1142c59 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,20 @@ 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 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); + } } /** Context of {@code block-right-click}: who clicked, and the click itself. */ @@ -256,4 +270,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/api/MaterialExtensions.java b/src/main/java/zone/moddev/mc/lib/api/MaterialExtensions.java index ffbf26f5..93b16ee0 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,28 @@ 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; unique among sources + * @param source supplies the definitions + * @throws IllegalArgumentException if a source with this name is already registered + */ + 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. */ + 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..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 @@ -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,52 @@ 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() + "'"; + 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(); - 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 +215,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 + ": " + origin + " and an earlier definition"); + 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 +259,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 + ": " + origin + " and an earlier definition"); + 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 +400,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 +436,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/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..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 @@ -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,124 @@ 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 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 + || !(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())); + } + + /** + * 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..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; @@ -15,6 +21,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 +54,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 +80,41 @@ 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 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; + } + /** Whether the block outputs any redstone signal. */ public boolean emits() { return stateOutput || constantOutput.filter(power -> power > 0).isPresent(); @@ -73,7 +132,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 +147,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 +182,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) { @@ -136,12 +237,20 @@ 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; 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 +268,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 +293,57 @@ protected void createBlockStateDefinition(StateDefinition.Builder 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); - server.setBlock(pos, updated, Block.UPDATE_CLIENTS); + if (previous != power) server.setBlock(pos, updated, Block.UPDATE_CLIENTS); var handler = ItemEventBridge.binding( definition.events(), material, MaterialEventTypes.REDSTONE_CHANGED.name()); @@ -233,11 +440,12 @@ private void checkInput(BlockState state, Level level, BlockPos pos) { new RedstoneChange(server, pos, updated, previous, power)); } - private void updateNeighbours(Level level, BlockPos pos) { + private void updateNeighbours(Level level, BlockPos pos, BlockState state) { level.updateNeighborsAt(pos, this); // A strong signal powers the touching block, so that block's own neighbours must update too. if (settings.strong()) - for (Direction face : settings.faces()) level.updateNeighborsAt(pos.relative(face), this); + for (Direction face : settings.faces()) + level.updateNeighborsAt(pos.relative(toWorld(state, face)), this); } private int emitted(BlockState state) { @@ -252,7 +460,7 @@ protected boolean isSignalSource(BlockState state) { // Direction points from the asking block towards this one, so the emitting face is its opposite. @Override protected int getSignal(BlockState state, BlockGetter level, BlockPos pos, Direction direction) { - return settings.faces().contains(direction.getOpposite()) ? emitted(state) : 0; + return settings.faces().contains(toLocal(state, direction.getOpposite())) ? emitted(state) : 0; } @Override diff --git a/src/main/java/zone/moddev/mc/lib/common/runtime/StandardBlocks.java b/src/main/java/zone/moddev/mc/lib/common/runtime/StandardBlocks.java index 48ddefe1..14938104 100644 --- a/src/main/java/zone/moddev/mc/lib/common/runtime/StandardBlocks.java +++ b/src/main/java/zone/moddev/mc/lib/common/runtime/StandardBlocks.java @@ -22,6 +22,36 @@ * Select actual vanilla or BMe-derived stateful classes; never fall back for an unknown behavior. */ public final class StandardBlocks { + /** + * {@code mmdlib:block} without redstone: a plain block that fires {@code block-stepped-on} and + * {@code block-random-tick}. + */ + public static class Plain extends Block { + private final BlockHooks hooks; + + /** A plain block whose events go to {@code hooks}. */ + public Plain(Properties properties, BlockHooks hooks) { + super(properties); + this.hooks = hooks; + } + + @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, + net.minecraft.server.level.ServerLevel level, + BlockPos pos, + net.minecraft.util.RandomSource random) { + super.randomTick(state, level, pos, random); + hooks.randomTick(level, pos, state); + } + } + private StandardBlocks() {} /** {@code mmdlib:bookshelf}: a block that powers enchanting tables like a vanilla bookshelf. */ diff --git a/src/main/java/zone/moddev/mc/lib/common/runtime/StandardImplementations.java b/src/main/java/zone/moddev/mc/lib/common/runtime/StandardImplementations.java index a1791637..543162a9 100644 --- a/src/main/java/zone/moddev/mc/lib/common/runtime/StandardImplementations.java +++ b/src/main/java/zone/moddev/mc/lib/common/runtime/StandardImplementations.java @@ -154,6 +154,8 @@ public static void install(MaterialExtensions api) { "fuel_" + c.part().templateId().getPath(), stats.integer("item_burn_time", 0, 0), 0), + c.definition().useTime(), + c.definition().useAnim(), c.properties()); }); item( @@ -178,7 +180,11 @@ public static void install(MaterialExtensions api) { settings -> 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..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 @@ -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,32 @@ 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) { + // 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)); + } } /** {@code mmdlib:bow}: a vanilla bow made of the material, repairable with it. */ @@ -502,7 +545,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/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..9121e2d5 --- /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, ScriptDefinitions.resourceOptions()) + : ScriptMaterialResources.data( + materials, blocks, items, ScriptDefinitions.itemTags(), ScriptDefinitions.resourceOptions()); + }); + 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..7dbff290 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockBuilder.java @@ -0,0 +1,163 @@ +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)); + } + + @ZenCodeType.Constructor + public ScriptBlockBuilder(String name, ScriptBlockModuleReference module) { + this(name, module.getId()); + } + + @ZenCodeType.Getter("reference") + public ScriptBlockReference getReference() { return new ScriptBlockReference(id.toString()); } + + private ScriptBlockBuilder(ResourceLocation id, ResourceLocation module) { + super(ScriptDefinitions.catalog().blockModule(module).template(), FactoryData.EMPTY); + this.id = id; + resourceOptions = ScriptDefinitions.resourceOptions("block_module", module); + 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(); + } + + @ZenCodeType.Method + public ScriptBlockBuilder baseBlockFrom(ScriptBlockReference block) { + setBaseBlock(block.getId()); return this; + } + + /** 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 + */ + @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( + 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)); + ScriptDefinitions.resources("block", id, resourceOptions); + } +} 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 new file mode 100644 index 00000000..28a7ffa2 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModule.java @@ -0,0 +1,158 @@ +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; + + @ZenCodeType.Constructor + public ScriptBlockModule(String name, ScriptBlockModuleReference basedOn) { + this(name, basedOn.getId()); + } + + @ZenCodeType.Getter("reference") + public ScriptBlockModuleReference getReference() { + return new ScriptBlockModuleReference(id.toString()); + } + + /** Snapshot current configuration under a new identity; source need not be built. */ + @ZenCodeType.Method + public ScriptBlockModule copy(String name) { + var targetId = mmdlibId(name, "block module"); + if (targetId.equals(id)) throw new IllegalArgumentException("Copy needs a different module ID"); + var snapshot = new DataModules.Block(factory, base.requiredMod(), + materialDefaults, factoryData(), template(), base.part()); + var target = new ScriptBlockModule(targetId, baseId, snapshot); + copyClassHooksTo(target); + target.resourceOptions = resourceOptions; + copyHooksTo(target); + return target; + } + + 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; + resourceOptions = ScriptDefinitions.resourceOptions("block_module", baseId); + 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 + */ + @Override + ResourceLocation definitionId() { + return id; + } + + @Override + ResourceLocation factoryId() { + return factory; + } + + @ZenCodeType.Method + @Override + public void build() { + validateBlock(); + 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.resources("block_module", id, resourceOptions); + ScriptDefinitions.module(id, baseId, java.util.Set.of()); + } +} diff --git a/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModuleReference.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModuleReference.java new file mode 100644 index 00000000..8378cc71 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockModuleReference.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.BlockModuleReference") +public final class ScriptBlockModuleReference { + private final ResourceLocation id; + + @ZenCodeType.Constructor + public ScriptBlockModuleReference(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/ScriptBlockProperties.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java new file mode 100644 index 00000000..d056207f --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockProperties.java @@ -0,0 +1,629 @@ +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, 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; + 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; + // 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 = ""; + // 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; + + 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"); + 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; + 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(); + 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(); + } + + @ZenCodeType.Method + public ScriptBlockProperties addTag(ScriptBlockTagReference tag) { + addTags(tag.getId()); + return this; + } + + @ZenCodeType.Method + public ScriptBlockProperties destroyTime(float value) { setHardness(value); return this; } + + @ZenCodeType.Method + public ScriptBlockProperties explosionResistance(float value) { setBlastResistance(value); return this; } + + @ZenCodeType.Method + public ScriptBlockProperties strength(float value) { return strength(value, value); } + + @ZenCodeType.Method + public ScriptBlockProperties strength(float hardness, float resistance) { + setHardness(hardness); setBlastResistance(resistance); return this; + } + + @ZenCodeType.Method + public ScriptBlockProperties noCollisions() { setCollision(false); return this; } + + @ZenCodeType.Method + public ScriptBlockProperties noOcclusion() { setOcclusion(false); return this; } + + @ZenCodeType.Method + public ScriptBlockProperties requiresToolToDrop() { return requiresToolToDrop(true); } + + @ZenCodeType.Method + public ScriptBlockProperties requiresToolToDrop(boolean value) { setRequiresCorrectTool(value); return this; } + + @ZenCodeType.Method + public ScriptBlockProperties noCorrespondingItem() { setHasItem(false); return this; } + + @ZenCodeType.Method + public ScriptBlockProperties noDrops() { + resourceOptions = resourceOptions.drops("none", ""); return this; + } + + @ZenCodeType.Method + public ScriptBlockProperties dropsNormally() { + resourceOptions = resourceOptions.drops("default", ""); return this; + } + + @ZenCodeType.Method + public ScriptBlockProperties dropsSelf() { + resourceOptions = resourceOptions.drops("self", ""); return this; + } + + @ZenCodeType.Method + public ScriptBlockProperties dropsFrom(String table) { + var id = ResourceLocation.parse(table); + var own = definitionId().withPrefix("blocks/"); + if (id.equals(own)) throw new IllegalArgumentException("Loot table cannot reference itself: " + id); + resourceOptions = resourceOptions.drops("table", id.toString()); return this; + } + + /** 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 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; + } + + /** Preserve class-only hook validation when snapshotting a module. */ + void copyClassHooksTo(ScriptBlockProperties target) { target.classHooks = classHooks; } + + /** + * 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 (!hasItem && resourceOptions.loot().equals("self")) + throw new IllegalArgumentException("dropsSelf requires a corresponding item"); + 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"); + 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); + // 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); + 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( + 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/ScriptBlockReference.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockReference.java new file mode 100644 index 00000000..db63eb08 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockReference.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.BlockReference") +public final class ScriptBlockReference { + private final ResourceLocation id; + + @ZenCodeType.Constructor + public ScriptBlockReference(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/ScriptBlockTagReference.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockTagReference.java new file mode 100644 index 00000000..b3e4c0bf --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptBlockTagReference.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.BlockTagReference") +public final class ScriptBlockTagReference { + private final ResourceLocation id; + + @ZenCodeType.Constructor + public ScriptBlockTagReference(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/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/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..5059797f --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitionProperties.java @@ -0,0 +1,210 @@ +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"; + ScriptResourceOptions resourceOptions = ScriptResourceOptions.DEFAULT; + + @ZenCodeType.Getter("displayName") + public String getDisplayName() { return resourceOptions.displayName(); } + + @ZenCodeType.Setter("displayName") + public void setDisplayName(String name) { resourceOptions = resourceOptions.named(name); } + + + 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 final Map> hookCopiers = new java.util.HashMap<>(); + 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(); + + /** 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) { + // 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)); + hookCopiers.put(event.name(), (target, value) -> { + @SuppressWarnings("unchecked") H snapshot = (H) value; + target.hook(event, snapshot, invoke); + }); + if (first) + 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()); + } + + /** 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() { + 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..a573896b --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptDefinitions.java @@ -0,0 +1,194 @@ +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 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. */ + 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(); + resources.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); + builtResources = Map.copyOf(resources); + 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 catalog == null ? builtItemTags : Map.copyOf(itemTags); + } + + /** 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/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 new file mode 100644 index 00000000..40b07a1b --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemBuilder.java @@ -0,0 +1,118 @@ +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)); + } + + @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; + } + + /** 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 + */ + @Override + ResourceLocation definitionId() { + return id; + } + + @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()); + ScriptDefinitions.resources("item", id, resourceOptions); + } +} 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 new file mode 100644 index 00000000..37c6ea1d --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemModule.java @@ -0,0 +1,137 @@ +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)); + } + + @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)); + } + + 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(); + } + + /** 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 + */ + @Override + ResourceLocation definitionId() { + return id; + } + + @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.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 new file mode 100644 index 00000000..f1fa4b5a --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptItemProperties.java @@ -0,0 +1,251 @@ +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.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, + * 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) +@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; + private int useDuration; + private String useAnimation; + + 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(); + 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. */ + @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)); + } + + /** 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( + base.name(), + base.parent(), + base.type(), + stackSize, + durability, + fireResistant, + base.food(), + container, + 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(), + events(), + base.group(), + base.creativeStacks(), + base.attributes()); + } +} 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/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/ScriptMaterialBuilder.java b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialBuilder.java new file mode 100644 index 00000000..bc04599f --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialBuilder.java @@ -0,0 +1,467 @@ +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 displayName = ""; + 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; + + 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. + */ + @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); + 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() { + return new MaterialRuntimeSettings( + repairItem, + incorrectBlocks, + baseBlock, + blockSetType, + name, + blockSound, + 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..526971f3 --- /dev/null +++ b/src/main/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResources.java @@ -0,0 +1,662 @@ +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) { + 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) { + 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 = 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(), 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(), ScriptResourceOptions.find(options, "block", block.name(), "block_module", part.templateId()).label(label(part.id(), display), 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(), 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. + .map(found -> found.equals("block") && directional(part) ? "block_facing" : found); + 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(), 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); + 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; + } + + /** 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" + + " 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) { + 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) { + 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; + var hasSlab = plan.blocks().keySet().stream().anyMatch(other -> other.id().equals("slab")); + 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); + 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)); + }); + 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; + } + + /** 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/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/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/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___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/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___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/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___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/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 00000000..88aece00 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___anvil_base.png differ 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 00000000..a2dbabc8 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_0.png differ 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 00000000..001883b1 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_1.png differ diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_2.png b/src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_2.png new file mode 100644 index 00000000..666738e5 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___anvil_top_damaged_2.png differ diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___bars.png b/src/main/resources/mmdlib_templates/textures/block/__material___bars.png new file mode 100644 index 00000000..2e83ac00 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___bars.png differ diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___block.png b/src/main/resources/mmdlib_templates/textures/block/__material___block.png new file mode 100644 index 00000000..06efb3fa Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___block.png differ diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___door_lower.png b/src/main/resources/mmdlib_templates/textures/block/__material___door_lower.png new file mode 100644 index 00000000..b8dd43a1 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___door_lower.png differ 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 00000000..3c43ee86 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___door_upper.png differ 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 00000000..e85508ad Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___ladder.png differ diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___ore_overlay.png b/src/main/resources/mmdlib_templates/textures/block/__material___ore_overlay.png new file mode 100644 index 00000000..fb3c65e6 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___ore_overlay.png differ 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 00000000..06efb3fa Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___plate.png differ diff --git a/src/main/resources/mmdlib_templates/textures/block/__material___trapdoor.png b/src/main/resources/mmdlib_templates/textures/block/__material___trapdoor.png new file mode 100644 index 00000000..bf065e2e Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/block/__material___trapdoor.png differ diff --git a/src/main/resources/mmdlib_templates/textures/entity/horse/armor/horse_armor___material__.png b/src/main/resources/mmdlib_templates/textures/entity/horse/armor/horse_armor___material__.png new file mode 100644 index 00000000..bfdf3aaf Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/entity/horse/armor/horse_armor___material__.png differ 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 00000000..ce689ceb Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___arrow.png differ 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 00000000..2c753c01 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___arrow.untinted.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___axe.png b/src/main/resources/mmdlib_templates/textures/item/__material___axe.png new file mode 100644 index 00000000..5946c937 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___axe.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___axe.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___axe.untinted.png new file mode 100644 index 00000000..ef7260e9 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___axe.untinted.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___blend.png b/src/main/resources/mmdlib_templates/textures/item/__material___blend.png new file mode 100644 index 00000000..4ab2f8cb Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___blend.png differ 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 00000000..f3bcb4ee Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___bolt.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___boots.png b/src/main/resources/mmdlib_templates/textures/item/__material___boots.png new file mode 100644 index 00000000..5411e0ef Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___boots.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_0.png b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_0.png new file mode 100644 index 00000000..0cd644ae Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_0.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_0.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_0.untinted.png new file mode 100644 index 00000000..4a35a80b Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_0.untinted.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_1.png b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_1.png new file mode 100644 index 00000000..0cd644ae Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_1.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_1.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_1.untinted.png new file mode 100644 index 00000000..4ae774b1 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_1.untinted.png differ 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 00000000..0cd644ae Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_2.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_2.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_2.untinted.png new file mode 100644 index 00000000..88e7cc7c Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___bow_pulling_2.untinted.png differ 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 00000000..0cd644ae Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___bow_standby.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___bow_standby.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___bow_standby.untinted.png new file mode 100644 index 00000000..5b3818fa Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___bow_standby.untinted.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___chestplate.png b/src/main/resources/mmdlib_templates/textures/item/__material___chestplate.png new file mode 100644 index 00000000..1cba8c4b Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___chestplate.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___clump.png b/src/main/resources/mmdlib_templates/textures/item/__material___clump.png new file mode 100644 index 00000000..98d2d73c Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___clump.png differ 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 00000000..24c9d6bb Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___crackhammer.png differ 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 00000000..e11e1029 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___crackhammer.untinted.png differ 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 00000000..e4edc552 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_0.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_0.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_0.untinted.png new file mode 100644 index 00000000..4a35a80b Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_0.untinted.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_1.png b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_1.png new file mode 100644 index 00000000..c122672f Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_1.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_1.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_1.untinted.png new file mode 100644 index 00000000..4ae774b1 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_1.untinted.png differ 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 00000000..b512b14d Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_2.png differ 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 00000000..88e7cc7c Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_pulling_2.untinted.png differ 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 00000000..5629130c Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_standby.png differ 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 00000000..5b3818fa Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___crossbow_standby.untinted.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___crystal.png b/src/main/resources/mmdlib_templates/textures/item/__material___crystal.png new file mode 100644 index 00000000..a6451dbd Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___crystal.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___door.png b/src/main/resources/mmdlib_templates/textures/item/__material___door.png new file mode 100644 index 00000000..8e7e02d9 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___door.png differ 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 00000000..0f459dbe Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___dust.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_cast.png b/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_cast.png new file mode 100644 index 00000000..ed1e7b2c Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_cast.png differ 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 00000000..278d096f Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_cast.untinted.png differ 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 00000000..ed1e7b2c Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_uncast.png differ 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 00000000..0d3dda97 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___fishing_rod_uncast.untinted.png differ 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 00000000..9beb8459 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___gear.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___gem.png b/src/main/resources/mmdlib_templates/textures/item/__material___gem.png new file mode 100644 index 00000000..a6451dbd Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___gem.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___helmet.png b/src/main/resources/mmdlib_templates/textures/item/__material___helmet.png new file mode 100644 index 00000000..a0e41dcf Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___helmet.png differ 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 00000000..e50e8d78 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___hoe.png differ 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 00000000..8e89c5ad Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___hoe.untinted.png differ 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 00000000..29781461 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___horse_armor.png differ 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 00000000..442efe91 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___ingot.png differ 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 00000000..33e422d6 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___leggings.png differ 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 00000000..c541b4ca Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___nugget.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___pickaxe.png b/src/main/resources/mmdlib_templates/textures/item/__material___pickaxe.png new file mode 100644 index 00000000..240973da Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___pickaxe.png differ 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 00000000..8e89c5ad Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___pickaxe.untinted.png differ 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 00000000..cccfc3b8 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___powder_dirty.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___rod.png b/src/main/resources/mmdlib_templates/textures/item/__material___rod.png new file mode 100644 index 00000000..13cef60d Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___rod.png differ 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 00000000..731832cc Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___scythe.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___scythe.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___scythe.untinted.png new file mode 100644 index 00000000..d31a2146 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___scythe.untinted.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___shard.png b/src/main/resources/mmdlib_templates/textures/item/__material___shard.png new file mode 100644 index 00000000..d89d58b6 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___shard.png differ 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 00000000..0e98a935 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___shears.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___shears.untinted.png b/src/main/resources/mmdlib_templates/textures/item/__material___shears.untinted.png new file mode 100644 index 00000000..a23a7bed Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___shears.untinted.png differ diff --git a/src/main/resources/mmdlib_templates/textures/item/__material___shovel.png b/src/main/resources/mmdlib_templates/textures/item/__material___shovel.png new file mode 100644 index 00000000..71c5761a Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___shovel.png differ 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 00000000..893e8985 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___shovel.untinted.png differ 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 00000000..d0166898 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___smallblend.png differ 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 00000000..c894f53e Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___smallpowder.png differ 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 00000000..f232f3b5 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___sword.png differ 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 00000000..6b5df57f Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/item/__material___sword.untinted.png differ 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 00000000..fdeea256 Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/models/armor/__material___layer_1.png differ diff --git a/src/main/resources/mmdlib_templates/textures/models/armor/__material___layer_2.png b/src/main/resources/mmdlib_templates/textures/models/armor/__material___layer_2.png new file mode 100644 index 00000000..6259212a Binary files /dev/null and b/src/main/resources/mmdlib_templates/textures/models/armor/__material___layer_2.png differ 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..dedea8a8 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"); + if (MMDLib.loadedMaterials().size() != 16) throw new AssertionError("Missing materials"); var detector = BuiltInRegistries.BLOCK.get(ResourceLocation.parse("mmdlib:human_detector")); - if (MMDLib.standaloneBlocks().size() != 1 + if (MMDLib.standaloneBlocks().size() != 12 || !(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, 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()); } @@ -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 != 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/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..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( @@ -113,6 +128,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..6bcd2ea8 --- /dev/null +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/RedstoneSmoke.java @@ -0,0 +1,79 @@ +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() {} + + 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; + 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(); + 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)) + .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..f1c1d715 --- /dev/null +++ b/src/runtimeTest/java/zone/moddev/mc/lib/smoke/ScriptHookSmoke.java @@ -0,0 +1,225 @@ +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; + } + + 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(); + 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/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/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/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/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 new file mode 100644 index 00000000..f3d5f301 --- /dev/null +++ b/src/test/java/zone/moddev/mc/lib/compat/CraftTweaker/ScriptMaterialResourcesTest.java @@ -0,0 +1,295 @@ +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"); + + @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() + .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"); + } + + @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( + 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); + } +} 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()); + } +} 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": {} }