Repository: neoforged/NeoForge
Pull request #2075: [1.21.5] Improve custom blockstate model datagen
Author: login: XFactHD
Labels: name: enhancement
description: New (or improvement to existing) feature or request
name: area: rendering
description: Related to rendering
name: breaking change
description: Breaks binary compatibility
name: area: data driven
description: This request involves a data driven system
name: 1.21.5
description: Targeted at Minecraft 1.21.5
Created: 2025-03-28T19:47:36Z | Merged: 2025-03-30T20:32:12Z

This PR is a follow-up to #2052 and improves the implementation of datagen support for `CustomUnbakedBlockStateModel`s.


Diff:
diff --git a/patches/net/minecraft/client/data/models/MultiVariant.java.patch b/patches/net/minecraft/client/data/models/MultiVariant.java.patch
index 054cd459248..03653377a64 100644
--- a/patches/net/minecraft/client/data/models/MultiVariant.java.patch
+++ b/patches/net/minecraft/client/data/models/MultiVariant.java.patch
@@ -1,45 +1,58 @@
 --- a/net/minecraft/client/data/models/MultiVariant.java
 +++ b/net/minecraft/client/data/models/MultiVariant.java
-@@ -12,20 +_,41 @@
+@@ -12,20 +_,54 @@
  import net.neoforged.api.distmarker.OnlyIn;
  
  @OnlyIn(Dist.CLIENT)
 -public record MultiVariant(WeightedList<Variant> variants) {
 -    public MultiVariant(WeightedList<Variant> variants) {
-+public record MultiVariant(WeightedList<Variant> variants, @org.jetbrains.annotations.Nullable net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder customBlockStateModel) {
++public record MultiVariant(WeightedList<Variant> variants, WeightedList<net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder> customBlockStateModels) {
 +    /**
-+     * @param customBlockStateModel Neo-added parameter to allow using custom blockstate models with MultiVariantGenerator and MultiPartGenerator (only used during datagen)
++     * @param customBlockStateModels Neo-added parameter to allow using custom blockstate models with MultiVariantGenerator and MultiPartGenerator (only used during datagen)
 +     */
-+    public MultiVariant(WeightedList<Variant> variants, @org.jetbrains.annotations.Nullable net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder customBlockStateModel) {
-+        if (customBlockStateModel != null) {
++    public MultiVariant(WeightedList<Variant> variants, WeightedList<net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder> customBlockStateModels) {
++        if (!customBlockStateModels.isEmpty()) {
 +            if (!variants.isEmpty()) {
 +                throw new IllegalArgumentException("Cannot specify both a variant list and a custom blockstate model");
 +            }
 +            this.variants = variants;
-+            this.customBlockStateModel = customBlockStateModel;
++            this.customBlockStateModels = customBlockStateModels;
 +        } else
          if (variants.isEmpty()) {
              throw new IllegalArgumentException("Variant list must contain at least one element");
          } else {
              this.variants = variants;
-+            this.customBlockStateModel = null;
++            this.customBlockStateModels = customBlockStateModels;
          }
 +    }
 +
 +    public MultiVariant(WeightedList<Variant> variants) {
-+        this(variants, null);
++        this(variants, WeightedList.of());
++    }
++
++    // Neo: convenience functions for datagen with custom blockstate models
++    public static MultiVariant of(WeightedList<net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder> customBlockStateModels) {
++        return new MultiVariant(WeightedList.of(), customBlockStateModels);
++    }
++
++    public static MultiVariant of(net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder customBlockStateModel) {
++        return new MultiVariant(WeightedList.of(), WeightedList.of(customBlockStateModel));
      }
  
      public MultiVariant with(VariantMutator p_409928_) {
-+        if (this.customBlockStateModel != null) {
-+            return new MultiVariant(this.variants, this.customBlockStateModel.with(p_409928_));
++        if (!this.customBlockStateModels.isEmpty()) {
++            return new MultiVariant(this.variants, this.customBlockStateModels.map(model -> model.with(p_409928_)));
 +        }
          return new MultiVariant(this.variants.map(p_409928_));
      }
  
      public BlockStateModel.Unbaked toUnbaked() {
-+        if (this.customBlockStateModel != null) {
-+            return this.customBlockStateModel.toUnbaked();
++        if (!this.customBlockStateModels.isEmpty()) {
++            var builders = this.customBlockStateModels.unwrap();
++            if (builders.size() == 1) {
++                return builders.getFirst().value().toUnbaked();
++            }
++            return new WeightedVariants.Unbaked(this.customBlockStateModels.map(net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder::toUnbaked));
 +        }
          List<Weighted<Variant>> list = this.variants.unwrap();
          return (BlockStateModel.Unbaked)(list.size() == 1
diff --git a/src/client/java/net/neoforged/neoforge/client/model/generators/blockstate/CompositeBlockStateModelBuilder.java b/src/client/java/net/neoforged/neoforge/client/model/generators/blockstate/CompositeBlockStateModelBuilder.java
index 9a64ce14208..f9e0e383989 100644
--- a/src/client/java/net/neoforged/neoforge/client/model/generators/blockstate/CompositeBlockStateModelBuilder.java
+++ b/src/client/java/net/neoforged/neoforge/client/model/generators/blockstate/CompositeBlockStateModelBuilder.java
@@ -7,32 +7,31 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 import net.minecraft.client.renderer.block.model.BlockStateModel;
 import net.minecraft.client.renderer.block.model.VariantMutator;
 import net.neoforged.neoforge.client.model.block.CompositeBlockModel;
-import org.jetbrains.annotations.Nullable;
+import net.neoforged.neoforge.client.model.block.CustomUnbakedBlockStateModel;
 
+/**
+ * Builder for creating {@link CompositeBlockModel.Unbaked}s in datagen
+ */
 public class CompositeBlockStateModelBuilder extends CustomBlockStateModelBuilder {
-    @Nullable
-    private CompositeBlockModel.Unbaked model = null;
+    private final List<BlockStateModel.Unbaked> models = new ArrayList<>();
 
+    /**
+     * Add a part model to this composite model
+     */
     public void addPartModel(BlockStateModel.Unbaked partModel) {
-        List<BlockStateModel.Unbaked> models = new ArrayList<>();
-        if (this.model != null) {
-            models.addAll(this.model.models());
-        }
-        models.add(partModel);
-        this.model = new CompositeBlockModel.Unbaked(models);
+        this.models.add(partModel);
     }
 
     @Override
-    public CustomBlockStateModelBuilder with(VariantMutator variantMutator) {
+    public CompositeBlockStateModelBuilder with(VariantMutator variantMutator) {
         return this;
     }
 
     @Override
-    public BlockStateModel.Unbaked toUnbaked() {
-        return Objects.requireNonNull(this.model, "Composite model has no parts");
+    public CustomUnbakedBlockStateModel toUnbaked() {
+        return new CompositeBlockModel.Unbaked(this.models);
     }
 }
diff --git a/src/client/java/net/neoforged/neoforge/client/model/generators/blockstate/CustomBlockStateModelBuilder.java b/src/client/java/net/neoforged/neoforge/client/model/generators/blockstate/CustomBlockStateModelBuilder.java
index 12675838549..d30716470da 100644
--- a/src/client/java/net/neoforged/neoforge/client/model/generators/blockstate/CustomBlockStateModelBuilder.java
+++ b/src/client/java/net/neoforged/neoforge/client/model/generators/blockstate/CustomBlockStateModelBuilder.java
@@ -5,16 +5,28 @@
 
 package net.neoforged.neoforge.client.model.generators.blockstate;
 
-import net.minecraft.client.renderer.block.model.BlockStateModel;
+import net.minecraft.client.data.models.MultiVariant;
+import net.minecraft.client.data.models.blockstates.MultiPartGenerator;
+import net.minecraft.client.data.models.blockstates.MultiVariantGenerator;
 import net.minecraft.client.renderer.block.model.VariantMutator;
 import net.neoforged.neoforge.client.model.block.CustomUnbakedBlockStateModel;
 
+/**
+ * Builder for {@link CustomUnbakedBlockStateModel}s to allow using them with {@link MultiVariantGenerator} and
+ * {@link MultiPartGenerator} by plugging them into {@link MultiVariant}
+ */
 public abstract class CustomBlockStateModelBuilder {
     protected CustomBlockStateModelBuilder() {}
 
+    /**
+     * Apply the provided {@link VariantMutator} to this builder
+     */
     public abstract CustomBlockStateModelBuilder with(VariantMutator variantMutator);
 
-    public abstract BlockStateModel.Unbaked toUnbaked();
+    /**
+     * Convert this builder to the final {@link CustomUnbakedBlockStateModel} for serialization
+     */
+    public abstract CustomUnbakedBlockStateModel toUnbaked();
 
     public static final class Simple extends CustomBlockStateModelBuilder {
         private final CustomUnbakedBlockStateModel blockStateModel;
@@ -29,7 +41,7 @@ public Simple with(VariantMutator variantMutator) {
         }
 
         @Override
-        public BlockStateModel.Unbaked toUnbaked() {
+        public CustomUnbakedBlockStateModel toUnbaked() {
             return this.blockStateModel;
         }
     }


Reviews:
user: login: Technici4n
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T08:49:41Z
user: login: XFactHD
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T23:29:14Z
user: login: shartte
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T14:54:17Z
user: login: Technici4n
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-30T20:31:52Z

Review comments:
diff_hunk: @@ -1,45 +1,54 @@
 --- a/net/minecraft/client/data/models/MultiVariant.java
 +++ b/net/minecraft/client/data/models/MultiVariant.java
-@@ -12,20 +_,41 @@
+@@ -12,20 +_,50 @@
  import net.neoforged.api.distmarker.OnlyIn;
  
  @OnlyIn(Dist.CLIENT)
 -public record MultiVariant(WeightedList<Variant> variants) {
 -    public MultiVariant(WeightedList<Variant> variants) {
-+public record MultiVariant(WeightedList<Variant> variants, @org.jetbrains.annotations.Nullable net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder customBlockStateModel) {
++public record MultiVariant(WeightedList<Variant> variants, WeightedList<net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder> customBlockStateModels) {
 +    /**
-+     * @param customBlockStateModel Neo-added parameter to allow using custom blockstate models with MultiVariantGenerator and MultiPartGenerator (only used during datagen)
++     * @param customBlockStateModels Neo-added parameter to allow using custom blockstate models with MultiVariantGenerator and MultiPartGenerator (only used during datagen)
 +     */
-+    public MultiVariant(WeightedList<Variant> variants, @org.jetbrains.annotations.Nullable net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder customBlockStateModel) {
-+        if (customBlockStateModel != null) {
++    public MultiVariant(WeightedList<Variant> variants, WeightedList<net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder> customBlockStateModels) {
++        if (!customBlockStateModels.isEmpty()) {
 +            if (!variants.isEmpty()) {
 +                throw new IllegalArgumentException("Cannot specify both a variant list and a custom blockstate model");
 +            }
 +            this.variants = variants;
-+            this.customBlockStateModel = customBlockStateModel;
++            this.customBlockStateModels = customBlockStateModels;
 +        } else
          if (variants.isEmpty()) {
              throw new IllegalArgumentException("Variant list must contain at least one element");
          } else {
              this.variants = variants;
-+            this.customBlockStateModel = null;
++            this.customBlockStateModels = customBlockStateModels;
          }
 +    }
 +
 +    public MultiVariant(WeightedList<Variant> variants) {
-+        this(variants, null);
++        this(variants, WeightedList.of());
++    }
++
++    // Neo: convenience function for datagen with custom blockstate models
++    public static MultiVariant of(WeightedList<net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder> customBlockStateModels) {
path: patches/net/minecraft/client/data/models/MultiVariant.java.patch
user: login: Technici4n
body: There should still be a function that accepts a single `CustomBlockStateModelBuilder`, no?
created_at: 2025-03-29T08:48:43Z
author_association: MEMBER
diff_hunk: @@ -1,45 +1,54 @@
 --- a/net/minecraft/client/data/models/MultiVariant.java
 +++ b/net/minecraft/client/data/models/MultiVariant.java
-@@ -12,20 +_,41 @@
+@@ -12,20 +_,50 @@
  import net.neoforged.api.distmarker.OnlyIn;
  
  @OnlyIn(Dist.CLIENT)
 -public record MultiVariant(WeightedList<Variant> variants) {
 -    public MultiVariant(WeightedList<Variant> variants) {
-+public record MultiVariant(WeightedList<Variant> variants, @org.jetbrains.annotations.Nullable net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder customBlockStateModel) {
++public record MultiVariant(WeightedList<Variant> variants, WeightedList<net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder> customBlockStateModels) {
 +    /**
-+     * @param customBlockStateModel Neo-added parameter to allow using custom blockstate models with MultiVariantGenerator and MultiPartGenerator (only used during datagen)
++     * @param customBlockStateModels Neo-added parameter to allow using custom blockstate models with MultiVariantGenerator and MultiPartGenerator (only used during datagen)
 +     */
-+    public MultiVariant(WeightedList<Variant> variants, @org.jetbrains.annotations.Nullable net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder customBlockStateModel) {
-+        if (customBlockStateModel != null) {
++    public MultiVariant(WeightedList<Variant> variants, WeightedList<net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder> customBlockStateModels) {
++        if (!customBlockStateModels.isEmpty()) {
 +            if (!variants.isEmpty()) {
 +                throw new IllegalArgumentException("Cannot specify both a variant list and a custom blockstate model");
 +            }
 +            this.variants = variants;
-+            this.customBlockStateModel = customBlockStateModel;
++            this.customBlockStateModels = customBlockStateModels;
 +        } else
          if (variants.isEmpty()) {
              throw new IllegalArgumentException("Variant list must contain at least one element");
          } else {
              this.variants = variants;
-+            this.customBlockStateModel = null;
++            this.customBlockStateModels = customBlockStateModels;
          }
 +    }
 +
 +    public MultiVariant(WeightedList<Variant> variants) {
-+        this(variants, null);
++        this(variants, WeightedList.of());
++    }
++
++    // Neo: convenience function for datagen with custom blockstate models
++    public static MultiVariant of(WeightedList<net.neoforged.neoforge.client.model.generators.blockstate.CustomBlockStateModelBuilder> customBlockStateModels) {
path: patches/net/minecraft/client/data/models/MultiVariant.java.patch
user: login: XFactHD
body: Sure, why not
created_at: 2025-03-29T23:29:14Z
author_association: MEMBER

PR comments:
user: login: neoforged-pr-publishing[bot]
created_at: 2025-03-28T19:47:48Z
body: 
- [x] Publish PR to GitHub Packages

Last commit published: [436d683793b908100c3b7a3db509d204c7347687](https://github.com/neoforged/NeoForge/commit/436d683793b908100c3b7a3db509d204c7347687).

<details>

<summary>PR Publishing</summary>

### The artifacts published by this PR:  
- :package: [`net.neoforged:neoforge:21.5.10-beta-pr-2075-blockstate_model_datagen`](https://github.com/neoforged/NeoForge/packages/2453506?version=21.5.10-beta-pr-2075-blockstate_model_datagen)
- :package: [`net.neoforged:testframework:21.5.10-beta-pr-2075-blockstate_model_datagen`](https://github.com/neoforged/NeoForge/packages/2453507?version=21.5.10-beta-pr-2075-blockstate_model_datagen)  

### Repository Declaration
In order to use the artifacts published by the PR, add the following repository to your buildscript:
```gradle
repositories {
    maven {
        name 'Maven for PR #2075' // https://github.com/neoforged/NeoForge/pull/2075
        url 'https://prmaven.neoforged.net/NeoForge/pr2075'
        content {
            includeModule('net.neoforged', 'neoforge')
            includeModule('net.neoforged', 'testframework')
        }
    }
}
```
### MDK installation
In order to setup a MDK using the latest PR version, run the following commands in a terminal.  
The script works on both *nix and Windows as long as you have the JDK `bin` folder on the path.  
The script will clone the MDK in a folder named `NeoForge-pr2075`.  
On Powershell you will need to remove the `-L` flag from the `curl` invocation.
```sh
mkdir NeoForge-pr2075
cd NeoForge-pr2075
curl -L https://prmaven.neoforged.net/NeoForge/pr2075/net/neoforged/neoforge/21.5.10-beta-pr-2075-blockstate_model_datagen/mdk-pr2075.zip -o mdk.zip
jar xf mdk.zip
rm mdk.zip || del mdk.zip
```

To test a production environment, you can download the installer from [here](https://prmaven.neoforged.net/NeoForge/pr2075/net.neoforged/neoforge/21.5.10-beta-pr-2075-blockstate_model_datagen/neoforge-21.5.10-beta-pr-2075-blockstate_model_datagen-installer.jar).

</details>
author_association: NONE
user: login: neoforged-compatibility-checks[bot]
created_at: 2025-03-28T19:51:56Z
body: @XFactHD, this PR introduces breaking changes.
Fortunately, this project is currently accepting breaking changes, but if they are not intentional, please revert them.
Last checked commit: [436d683793b908100c3b7a3db509d204c7347687](https://github.com/neoforged/NeoForge/commit/436d683793b908100c3b7a3db509d204c7347687).


<details>

<summary>Compatibility checks</summary>


## `neoforge (:neoforge)`
  - `net/neoforged/neoforge/client/model/generators/blockstate/CompositeBlockStateModelBuilder`
    * `toUnbaked()Lnet/minecraft/client/renderer/block/model/BlockStateModel$Unbaked;`: ❗ API method was removed
  - `net/minecraft/client/data/models/MultiVariant`
    * `<init>(Lnet/minecraft/util/random/WeightedList;Lnet/neoforged/neoforge/client/model/generators/blockstate/CustomBlockStateModelBuilder;)V`: ❗ API method was removed
    * `customBlockStateModel()Lnet/neoforged/neoforge/client/model/generators/blockstate/CustomBlockStateModelBuilder;`: ❗ API method was removed
  - `net/neoforged/neoforge/client/model/generators/blockstate/CustomBlockStateModelBuilder`
    * `toUnbaked()Lnet/neoforged/neoforge/client/model/block/CustomUnbakedBlockStateModel;`: ❗ Method was made abstract
    * `toUnbaked()Lnet/minecraft/client/renderer/block/model/BlockStateModel$Unbaked;`: ❗ API method was removed
  - `net/neoforged/neoforge/client/model/generators/blockstate/CustomBlockStateModelBuilder$Simple`
    * `toUnbaked()Lnet/minecraft/client/renderer/block/model/BlockStateModel$Unbaked;`: ❗ API method was removed


</details>
author_association: NONE
user: login: neoforged-releases[bot]
created_at: 2025-03-30T20:35:29Z
body: 🚀 This PR has been released as NeoForge version `21.5.10-beta`.
author_association: NONE