Repository: PaperMC/Paper
Pull request #12417: Registry API for supported Mob Variants
Author: login: BertTowne
Created: 2025-04-12T22:49:51Z | Merged: 2025-05-06T21:13:00Z

Re-upload of #12364


Diff:
diff --git a/paper-api/src/main/java/io/papermc/paper/registry/data/CatTypeRegistryEntry.java b/paper-api/src/main/java/io/papermc/paper/registry/data/CatTypeRegistryEntry.java
new file mode 100644
index 000000000000..b818d61cd66d
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/registry/data/CatTypeRegistryEntry.java
@@ -0,0 +1,45 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.RegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import org.bukkit.entity.Cat;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * A data-centric version-specific registry entry for the {@link Cat.Type} type.
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface CatTypeRegistryEntry {
+
+    /**
+     * Provides the client texture asset of the cat type, which represents the texture to use.
+     *
+     * @return the client texture asset.
+     */
+    ClientTextureAsset clientTextureAsset();
+
+    /**
+     * A mutable builder for the {@link CatTypeRegistryEntry} plugins may change in applicable registry events.
+     * <p>
+     * The following values are required for each builder:
+     * <ul>
+     *     <li>{@link #clientTextureAsset(ClientTextureAsset)}</li>
+     * </ul>
+     */
+    @ApiStatus.Experimental
+    @ApiStatus.NonExtendable
+    interface Builder extends CatTypeRegistryEntry, RegistryBuilder<Cat.Type> {
+
+        /**
+         * Sets the client texture asset of the cat type, which is the location of the texture to use.
+         *
+         * @param clientTextureAsset the client texture asset.
+         * @return this builder instance.
+         * @see CatTypeRegistryEntry#clientTextureAsset()
+         */
+        @Contract(value = "_ -> this", mutates = "this")
+        Builder clientTextureAsset(ClientTextureAsset clientTextureAsset);
+    }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/registry/data/ChickenVariantRegistryEntry.java b/paper-api/src/main/java/io/papermc/paper/registry/data/ChickenVariantRegistryEntry.java
new file mode 100644
index 000000000000..104c01ddc353
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/registry/data/ChickenVariantRegistryEntry.java
@@ -0,0 +1,78 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.RegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import org.bukkit.entity.Chicken;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * A data-centric version-specific registry entry for the {@link Chicken.Variant} type.
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface ChickenVariantRegistryEntry {
+
+    /**
+     * The model of the chicken variant to render the configured texture on.
+     */
+    enum Model {
+        /**
+         * The normal chicken model.
+         */
+        NORMAL,
+
+        /**
+         * The cold chicken model.
+         */
+        COLD,
+    }
+
+    /**
+     * Provides the client texture asset of the chicken variant, which represents the texture to use.
+     *
+     * @return the client texture asset.
+     */
+    ClientTextureAsset clientTextureAsset();
+
+    /**
+     * Provides the model of the chicken variant.
+     *
+     * @return the model.
+     */
+    Model model();
+
+    /**
+     * A mutable builder for the {@link ChickenVariantRegistryEntry} plugins may change in applicable registry events.
+     * <p>
+     * The following values are required for each builder:
+     * <ul>
+     *     <li>{@link #clientTextureAsset(ClientTextureAsset)}</li>
+     *     <li>{@link #model(Model)}</li>
+     * </ul>
+     */
+    @ApiStatus.Experimental
+    @ApiStatus.NonExtendable
+    interface Builder extends ChickenVariantRegistryEntry, RegistryBuilder<Chicken.Variant> {
+
+        /**
+         * Sets the client texture asset of the chicken variant, which is the location of the texture to use.
+         *
+         * @param clientTextureAsset the client texture asset.
+         * @return this builder instance.
+         * @see ChickenVariantRegistryEntry#clientTextureAsset()
+         */
+        @Contract(value = "_ -> this", mutates = "this")
+        Builder clientTextureAsset(ClientTextureAsset clientTextureAsset);
+
+        /**
+         * Sets the model to use for this chicken variant.
+         *
+         * @param model the model.
+         * @return this builder instance.
+         * @see ChickenVariantRegistryEntry#model()
+         */
+        @Contract(value = "_ -> this", mutates = "this")
+        Builder model(Model model);
+    }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/registry/data/CowVariantRegistryEntry.java b/paper-api/src/main/java/io/papermc/paper/registry/data/CowVariantRegistryEntry.java
new file mode 100644
index 000000000000..dce4a959df41
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/registry/data/CowVariantRegistryEntry.java
@@ -0,0 +1,83 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.RegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import org.bukkit.entity.Cow;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * A data-centric version-specific registry entry for the {@link Cow.Variant} type.
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface CowVariantRegistryEntry {
+
+    /**
+     * The model of the cow variant to render the configured texture on.
+     */
+    enum Model {
+        /**
+         * The normal cow model.
+         */
+        NORMAL,
+
+        /**
+         * The cold cow model.
+         */
+        COLD,
+
+        /**
+         * The warm cow model.
+         */
+        WARM,
+    }
+
+    /**
+     * Provides the client texture asset of the cow variant, which represents the texture to use.
+     *
+     * @return the client texture asset.
+     */
+    ClientTextureAsset clientTextureAsset();
+
+    /**
+     * Provides the model of the cow variant.
+     *
+     * @return the model.
+     */
+    Model model();
+
+    /**
+     * A mutable builder for the {@link CowVariantRegistryEntry} plugins may change in applicable registry events.
+     * <p>
+     * The following values are required for each builder:
+     * <ul>
+     *     <li>{@link #clientTextureAsset(ClientTextureAsset)}</li>
+     *     <li>{@link #model(Model)}</li>
+     * </ul>
+     */
+    @ApiStatus.Experimental
+    @ApiStatus.NonExtendable
+    interface Builder extends CowVariantRegistryEntry, RegistryBuilder<Cow.Variant> {
+
+        /**
+         * Sets the client texture asset of the cow variant, which is the location of the texture to use.
+         *
+         * @param clientTextureAsset the client texture asset.
+         * @return this builder instance.
+         * @see CowVariantRegistryEntry#clientTextureAsset()
+         */
+        @Contract(value = "_ -> this", mutates = "this")
+        Builder clientTextureAsset(ClientTextureAsset clientTextureAsset);
+
+        /**
+         * Sets the model to use for this cow variant.
+         *
+         * @param model the model.
+         * @return this builder instance.
+         * @see CowVariantRegistryEntry#model()
+         */
+        @Contract(value = "_ -> this", mutates = "this")
+        Builder model(Model model);
+    }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/registry/data/FrogVariantRegistryEntry.java b/paper-api/src/main/java/io/papermc/paper/registry/data/FrogVariantRegistryEntry.java
new file mode 100644
index 000000000000..7f9c5e87602f
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/registry/data/FrogVariantRegistryEntry.java
@@ -0,0 +1,45 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.RegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import org.bukkit.entity.Frog;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * A data-centric version-specific registry entry for the {@link Frog.Variant} type.
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface FrogVariantRegistryEntry {
+
+    /**
+     * Provides the client texture asset of the frog variant, which represents the texture to use.
+     *
+     * @return the client texture asset.
+     */
+    ClientTextureAsset clientTextureAsset();
+
+    /**
+     * A mutable builder for the {@link FrogVariantRegistryEntry} plugins may change in applicable registry events.
+     * <p>
+     * The following values are required for each builder:
+     * <ul>
+     *     <li>{@link #clientTextureAsset(ClientTextureAsset)}</li>
+     * </ul>
+     */
+    @ApiStatus.Experimental
+    @ApiStatus.NonExtendable
+    interface Builder extends FrogVariantRegistryEntry, RegistryBuilder<Frog.Variant> {
+
+        /**
+         * Sets the client texture asset of the frog variant, which is the location of the texture to use.
+         *
+         * @param clientTextureAsset the client texture asset.
+         * @return this builder instance.
+         * @see FrogVariantRegistryEntry#clientTextureAsset()
+         */
+        @Contract(value = "_ -> this", mutates = "this")
+        Builder clientTextureAsset(ClientTextureAsset clientTextureAsset);
+    }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/registry/data/PigVariantRegistryEntry.java b/paper-api/src/main/java/io/papermc/paper/registry/data/PigVariantRegistryEntry.java
new file mode 100644
index 000000000000..02c043e835d7
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/registry/data/PigVariantRegistryEntry.java
@@ -0,0 +1,78 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.RegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import org.bukkit.entity.Pig;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * A data-centric version-specific registry entry for the {@link Pig.Variant} type.
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface PigVariantRegistryEntry {
+
+    /**
+     * The model of the pig variant to render the configured texture on.
+     */
+    enum Model {
+        /**
+         * The normal pig model.
+         */
+        NORMAL,
+
+        /**
+         * The cold pig model.
+         */
+        COLD,
+    }
+
+    /**
+     * Provides the client texture asset of the pig variant, which represents the texture to use.
+     *
+     * @return the client texture asset.
+     */
+    ClientTextureAsset clientTextureAsset();
+
+    /**
+     * Provides the model of the pig variant.
+     *
+     * @return the model.
+     */
+    Model model();
+
+    /**
+     * A mutable builder for the {@link PigVariantRegistryEntry} plugins may change in applicable registry events.
+     * <p>
+     * The following values are required for each builder:
+     * <ul>
+     *     <li>{@link #clientTextureAsset(ClientTextureAsset)}</li>
+     *     <li>{@link #model(Model)}</li>
+     * </ul>
+     */
+    @ApiStatus.Experimental
+    @ApiStatus.NonExtendable
+    interface Builder extends PigVariantRegistryEntry, RegistryBuilder<Pig.Variant> {
+
+        /**
+         * Sets the client texture asset of the pig variant, which is the location of the texture to use.
+         *
+         * @param clientTextureAsset the client texture asset.
+         * @return this builder instance.
+         * @see PigVariantRegistryEntry#clientTextureAsset()
+         */
+        @Contract(value = "_ -> this", mutates = "this")
+        Builder clientTextureAsset(ClientTextureAsset clientTextureAsset);
+
+        /**
+         * Sets the model to use for this pig variant.
+         *
+         * @param model the model.
+         * @return this builder instance.
+         * @see PigVariantRegistryEntry#model()
+         */
+        @Contract(value = "_ -> this", mutates = "this")
+        Builder model(Model model);
+    }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/registry/data/WolfVariantRegistryEntry.java b/paper-api/src/main/java/io/papermc/paper/registry/data/WolfVariantRegistryEntry.java
new file mode 100644
index 000000000000..786b8bd822c7
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/registry/data/WolfVariantRegistryEntry.java
@@ -0,0 +1,81 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.RegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import org.bukkit.entity.Wolf;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * A data-centric version-specific registry entry for the {@link Wolf.Variant} type.
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface WolfVariantRegistryEntry {
+
+    /**
+     * Provides the client texture asset of the wolf variant for when it is angry, which is the location of the texture to use.
+     *
+     * @return the client texture asset.
+     */
+    ClientTextureAsset angryClientTextureAsset();
+
+    /**
+     * Provides the client texture asset of the wolf variant for when it is wild, which is the location of the texture to use.
+     *
+     * @return the client texture asset.
+     */
+    ClientTextureAsset wildClientTextureAsset();
+
+    /**
+     * Provides the client texture asset of the wolf variant for when it is tame, which is the location of the texture to use.
+     *
+     * @return the client texture asset.
+     */
+    ClientTextureAsset tameClientTextureAsset();
+
+    /**
+     * A mutable builder for the {@link WolfVariantRegistryEntry} plugins may change in applicable registry events.
+     * <p>
+     * The following values are required for each builder:
+     * <ul>
+     *     <li>{@link #angryClientTextureAsset(ClientTextureAsset)}</li>
+     *     <li>{@link #wildClientTextureAsset(ClientTextureAsset)}</li>
+     *     <li>{@link #tameClientTextureAsset(ClientTextureAsset)}</li>
+     * </ul>
+     */
+    @ApiStatus.Experimental
+    @ApiStatus.NonExtendable
+    interface Builder extends WolfVariantRegistryEntry, RegistryBuilder<Wolf.Variant> {
+
+        /**
+         * Sets the client texture asset of the wolf variant for when it is angry, which is the location of the texture to use.
+         *
+         * @param angryClientTextureAsset the client texture asset.
+         * @return this builder instance.
+         * @see WolfVariantRegistryEntry#angryClientTextureAsset()
+         */
+        @Contract(value = "_ -> this", mutates = "this")
+        Builder angryClientTextureAsset(ClientTextureAsset angryClientTextureAsset);
+
+        /**
+         * Sets the client texture asset of the wolf variant for when it is wild, which is the location of the texture to use.
+         *
+         * @param wildClientTextureAsset the client texture asset.
+         * @return this builder instance.
+         * @see WolfVariantRegistryEntry#wildClientTextureAsset()
+         */
+        @Contract(value = "_ -> this", mutates = "this")
+        Builder wildClientTextureAsset(ClientTextureAsset wildClientTextureAsset);
+
+        /**
+         * Sets the client texture asset of the wolf variant for when it is tame, which is the location of the texture to use.
+         *
+         * @param tameClientTextureAsset the client texture asset.
+         * @return this builder instance.
+         * @see WolfVariantRegistryEntry#tameClientTextureAsset()
+         */
+        @Contract(value = "_ -> this", mutates = "this")
+        Builder tameClientTextureAsset(ClientTextureAsset tameClientTextureAsset);
+    }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/registry/data/client/ClientTextureAsset.java b/paper-api/src/main/java/io/papermc/paper/registry/data/client/ClientTextureAsset.java
new file mode 100644
index 000000000000..715f1759ea02
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/registry/data/client/ClientTextureAsset.java
@@ -0,0 +1,66 @@
+package io.papermc.paper.registry.data.client;
+
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.key.KeyPattern;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * A data-centric version-specific representation of a client texture asset, composed of an identifier and a path.
+ * Follows the same, version-specific, compatibility guarantees as the RegistryEntry types it is used in.
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface ClientTextureAsset {
+
+    /**
+     * The identifier of the client texture asset, uniquely identifying the asset on the client.
+     *
+     * @return the identifier.
+     */
+    Key identifier();
+
+    /**
+     * The path of the texture on the client, split into a namespace and a path/key.
+     *
+     * @return the texture path.
+     */
+    Key texturePath();
+
+    /**
+     * Creates a new {@link ClientTextureAsset} with the specified identifier and texture path.
+     *
+     * @param identifier  the unique identifier for the client texture asset.
+     * @param texturePath the path where the asset is located on the client.
+     * @return a new {@code ClientAsset} instance.
+     */
+    @Contract("_,_ -> new")
+    static ClientTextureAsset clientTextureAsset(final Key identifier, final Key texturePath) {
+        return new ClientTextureAssetImpl(identifier, texturePath);
+    }
+
+    /**
+     * Creates a new {@link ClientTextureAsset} using the provided identifier and infers the texture path from it.
+     *
+     * @param identifier the unique identifier for the client texture asset.
+     * @return a new {@code ClientAsset} instance.
+     */
+    @Contract("_ -> new")
+    static ClientTextureAsset clientTextureAsset(final Key identifier) {
+        return new ClientTextureAssetImpl(identifier, ClientTextureAssetImpl.pathFromIdentifier(identifier));
+    }
+
+    /**
+     * Creates a new {@link ClientTextureAsset} from the provided string-formatted {@link Key}
+     * and infers the texture path from it.
+     * <p>
+     * The identifier string must conform to the {@link KeyPattern} format.
+     *
+     * @param identifier the string representation of the asset's identifier.
+     * @return a new {@code ClientAsset} instance.
+     */
+    @Contract("_ -> new")
+    static ClientTextureAsset clientTextureAsset(final @KeyPattern String identifier) {
+        return clientTextureAsset(Key.key(identifier));
+    }
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/registry/data/client/ClientTextureAssetImpl.java b/paper-api/src/main/java/io/papermc/paper/registry/data/client/ClientTextureAssetImpl.java
new file mode 100644
index 000000000000..ad51a33a6f74
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/registry/data/client/ClientTextureAssetImpl.java
@@ -0,0 +1,31 @@
+package io.papermc.paper.registry.data.client;
+
+import net.kyori.adventure.key.Key;
+import org.intellij.lang.annotations.Subst;
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Package local implementation of the {@link ClientTextureAsset} type.
+ * Chosen over bridging into server internals as no internal types are required for this.
+ */
+@NullMarked
+record ClientTextureAssetImpl(
+    Key identifier,
+    Key texturePath
+) implements ClientTextureAsset {
+
+    /**
+     * Constructs the default asset path from the identifier of the asset.
+     * Mirrors internal logic in net.minecraft.core.ClientAsset
+     *
+     * @param identifier the identifier of the asset.
+     * @return the key/path of the asset.
+     */
+    static Key pathFromIdentifier(@Subst("") final Key identifier) {
+        return Key.key(
+            identifier.namespace(),
+            "textures/" + identifier.value() + ".png"
+        );
+    }
+
+}
diff --git a/paper-api/src/main/java/io/papermc/paper/registry/event/RegistryEvents.java b/paper-api/src/main/java/io/papermc/paper/registry/event/RegistryEvents.java
index 71b24560620c..0cf04f0e2b25 100644
--- a/paper-api/src/main/java/io/papermc/paper/registry/event/RegistryEvents.java
+++ b/paper-api/src/main/java/io/papermc/paper/registry/event/RegistryEvents.java
@@ -2,15 +2,27 @@
 
 import io.papermc.paper.registry.RegistryKey;
 import io.papermc.paper.registry.data.BannerPatternRegistryEntry;
+import io.papermc.paper.registry.data.CatTypeRegistryEntry;
+import io.papermc.paper.registry.data.ChickenVariantRegistryEntry;
+import io.papermc.paper.registry.data.CowVariantRegistryEntry;
 import io.papermc.paper.registry.data.DamageTypeRegistryEntry;
 import io.papermc.paper.registry.data.EnchantmentRegistryEntry;
+import io.papermc.paper.registry.data.FrogVariantRegistryEntry;
 import io.papermc.paper.registry.data.GameEventRegistryEntry;
 import io.papermc.paper.registry.data.PaintingVariantRegistryEntry;
+import io.papermc.paper.registry.data.PigVariantRegistryEntry;
+import io.papermc.paper.registry.data.WolfVariantRegistryEntry;
 import org.bukkit.Art;
 import org.bukkit.GameEvent;
 import org.bukkit.block.banner.PatternType;
 import org.bukkit.damage.DamageType;
 import org.bukkit.enchantments.Enchantment;
+import org.bukkit.entity.Cat;
+import org.bukkit.entity.Chicken;
+import org.bukkit.entity.Cow;
+import org.bukkit.entity.Frog;
+import org.bukkit.entity.Pig;
+import org.bukkit.entity.Wolf;
 import org.jetbrains.annotations.ApiStatus;
 import org.jspecify.annotations.NullMarked;
 
@@ -28,9 +40,15 @@ public final class RegistryEvents {
     // @GeneratedFrom 1.21.5
     public static final RegistryEventProvider<GameEvent, GameEventRegistryEntry.Builder> GAME_EVENT = create(RegistryKey.GAME_EVENT);
     public static final RegistryEventProvider<DamageType, DamageTypeRegistryEntry.Builder> DAMAGE_TYPE = create(RegistryKey.DAMAGE_TYPE);
+    public static final RegistryEventProvider<Wolf.Variant, WolfVariantRegistryEntry.Builder> WOLF_VARIANT = create(RegistryKey.WOLF_VARIANT);
     public static final RegistryEventProvider<Enchantment, EnchantmentRegistryEntry.Builder> ENCHANTMENT = create(RegistryKey.ENCHANTMENT);
     public static final RegistryEventProvider<PatternType, BannerPatternRegistryEntry.Builder> BANNER_PATTERN = create(RegistryKey.BANNER_PATTERN);
     public static final RegistryEventProvider<Art, PaintingVariantRegistryEntry.Builder> PAINTING_VARIANT = create(RegistryKey.PAINTING_VARIANT);
+    public static final RegistryEventProvider<Cat.Type, CatTypeRegistryEntry.Builder> CAT_VARIANT = create(RegistryKey.CAT_VARIANT);
+    public static final RegistryEventProvider<Frog.Variant, FrogVariantRegistryEntry.Builder> FROG_VARIANT = create(RegistryKey.FROG_VARIANT);
+    public static final RegistryEventProvider<Chicken.Variant, ChickenVariantRegistryEntry.Builder> CHICKEN_VARIANT = create(RegistryKey.CHICKEN_VARIANT);
+    public static final RegistryEventProvider<Cow.Variant, CowVariantRegistryEntry.Builder> COW_VARIANT = create(RegistryKey.COW_VARIANT);
+    public static final RegistryEventProvider<Pig.Variant, PigVariantRegistryEntry.Builder> PIG_VARIANT = create(RegistryKey.PIG_VARIANT);
     // End generate - RegistryEvents
 
     private RegistryEvents() {
diff --git a/paper-api/src/main/java/org/bukkit/entity/Wolf.java b/paper-api/src/main/java/org/bukkit/entity/Wolf.java
index a1c7b6ba655f..a6414ffc4a6d 100644
--- a/paper-api/src/main/java/org/bukkit/entity/Wolf.java
+++ b/paper-api/src/main/java/org/bukkit/entity/Wolf.java
@@ -5,7 +5,6 @@
 import org.bukkit.DyeColor;
 import org.bukkit.Keyed;
 import org.bukkit.NamespacedKey;
-import org.bukkit.Registry;
 import org.jetbrains.annotations.NotNull;
 
 /**
diff --git a/paper-generator/src/main/java/io/papermc/generator/registry/RegistryEntries.java b/paper-generator/src/main/java/io/papermc/generator/registry/RegistryEntries.java
index c12cdc0b1e78..38f8c83e8d61 100644
--- a/paper-generator/src/main/java/io/papermc/generator/registry/RegistryEntries.java
+++ b/paper-generator/src/main/java/io/papermc/generator/registry/RegistryEntries.java
@@ -4,8 +4,12 @@
 import io.papermc.paper.datacomponent.DataComponentType;
 import io.papermc.paper.datacomponent.DataComponentTypes;
 import io.papermc.paper.registry.data.BannerPatternRegistryEntry;
+import io.papermc.paper.registry.data.CatTypeRegistryEntry;
+import io.papermc.paper.registry.data.ChickenVariantRegistryEntry;
+import io.papermc.paper.registry.data.CowVariantRegistryEntry;
 import io.papermc.paper.registry.data.DamageTypeRegistryEntry;
 import io.papermc.paper.registry.data.EnchantmentRegistryEntry;
+import io.papermc.paper.registry.data.FrogVariantRegistryEntry;
 import io.papermc.paper.registry.data.GameEventRegistryEntry;
 import io.papermc.paper.registry.data.PaintingVariantRegistryEntry;
 import java.lang.reflect.Field;
@@ -20,6 +24,8 @@
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
+import io.papermc.paper.registry.data.PigVariantRegistryEntry;
+import io.papermc.paper.registry.data.WolfVariantRegistryEntry;
 import net.minecraft.core.Registry;
 import net.minecraft.core.component.DataComponents;
 import net.minecraft.core.particles.ParticleTypes;
@@ -161,18 +167,18 @@ private static <T> RegistryEntry<T> inconsistentEntry(ResourceKey<? extends Regi
         entry(Registries.TRIM_MATERIAL, TrimMaterials.class, TrimMaterial.class).allowDirect().delayed(),
         entry(Registries.TRIM_PATTERN, TrimPatterns.class, TrimPattern.class).allowDirect().delayed(),
         entry(Registries.DAMAGE_TYPE, DamageTypes.class, DamageType.class).apiRegistryBuilder(DamageTypeRegistryEntry.Builder.class, "PaperDamageTypeRegistryEntry.PaperBuilder").delayed(),
-        entry(Registries.WOLF_VARIANT, WolfVariants.class, Wolf.Variant.class).delayed(),
+        entry(Registries.WOLF_VARIANT, WolfVariants.class, Wolf.Variant.class).apiRegistryBuilder(WolfVariantRegistryEntry.Builder.class, "PaperWolfVariantRegistryEntry.PaperBuilder").delayed(),
         entry(Registries.WOLF_SOUND_VARIANT, WolfSoundVariants.class, Wolf.SoundVariant.class),
         entry(Registries.ENCHANTMENT, Enchantments.class, Enchantment.class).apiRegistryBuilder(EnchantmentRegistryEntry.Builder.class, "PaperEnchantmentRegistryEntry.PaperBuilder").serializationUpdater("ENCHANTMENT_RENAME").delayed(),
         entry(Registries.JUKEBOX_SONG, JukeboxSongs.class, JukeboxSong.class).delayed(),
         entry(Registries.BANNER_PATTERN, BannerPatterns.class, PatternType.class).allowDirect().apiRegistryBuilder(BannerPatternRegistryEntry.Builder.class, "PaperBannerPatternRegistryEntry.PaperBuilder").delayed(),
         entry(Registries.PAINTING_VARIANT, PaintingVariants.class, Art.class).allowDirect().apiRegistryBuilder(PaintingVariantRegistryEntry.Builder.class, "PaperPaintingVariantRegistryEntry.PaperBuilder").apiRegistryField("ART").delayed(),
         entry(Registries.INSTRUMENT, Instruments.class, MusicInstrument.class).allowDirect().delayed(),
-        entry(Registries.CAT_VARIANT, CatVariants.class, Cat.Type.class).delayed(),
-        entry(Registries.FROG_VARIANT, FrogVariants.class, Frog.Variant.class).delayed(),
-        entry(Registries.CHICKEN_VARIANT, ChickenVariants.class, Chicken.Variant.class),
-        entry(Registries.COW_VARIANT, CowVariants.class, Cow.Variant.class),
-        entry(Registries.PIG_VARIANT, PigVariants.class, Pig.Variant.class)
+        entry(Registries.CAT_VARIANT, CatVariants.class, Cat.Type.class).apiRegistryBuilder(CatTypeRegistryEntry.Builder.class, "PaperCatTypeRegistryEntry.PaperBuilder").delayed(),
+        entry(Registries.FROG_VARIANT, FrogVariants.class, Frog.Variant.class).apiRegistryBuilder(FrogVariantRegistryEntry.Builder.class, "PaperFrogVariantRegistryEntry.PaperBuilder").delayed(),
+        entry(Registries.CHICKEN_VARIANT, ChickenVariants.class, Chicken.Variant.class).apiRegistryBuilder(ChickenVariantRegistryEntry.Builder.class, "PaperChickenVariantRegistryEntry.PaperBuilder"),
+        entry(Registries.COW_VARIANT, CowVariants.class, Cow.Variant.class).apiRegistryBuilder(CowVariantRegistryEntry.Builder.class, "PaperCowVariantRegistryEntry.PaperBuilder"),
+        entry(Registries.PIG_VARIANT, PigVariants.class, Pig.Variant.class).apiRegistryBuilder(PigVariantRegistryEntry.Builder.class, "PaperPigVariantRegistryEntry.PaperBuilder")
     );
 
     public static final List<RegistryEntry<?>> API_ONLY = List.of(
diff --git a/paper-server/patches/sources/net/minecraft/core/ClientAsset.java.patch b/paper-server/patches/sources/net/minecraft/core/ClientAsset.java.patch
new file mode 100644
index 000000000000..3f686beec60c
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/core/ClientAsset.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/core/ClientAsset.java
++++ b/net/minecraft/core/ClientAsset.java
+@@ -12,6 +_,6 @@
+     public static final StreamCodec<ByteBuf, ClientAsset> STREAM_CODEC = StreamCodec.composite(ResourceLocation.STREAM_CODEC, ClientAsset::id, ClientAsset::new);
+ 
+     public ClientAsset(ResourceLocation id) {
+-        this(id, id.withPath(string -> "textures/" + string + ".png"));
++        this(id, id.withPath(string -> "textures/" + string + ".png")); // Paper - diff on change - io.papermc.paper.registry.data.client.ClientAssetImpl#pathFromIdentifier
+     }
+ }
diff --git a/paper-server/src/main/java/io/papermc/paper/registry/PaperRegistries.java b/paper-server/src/main/java/io/papermc/paper/registry/PaperRegistries.java
index 01dbd27a3dea..b0d34b749670 100644
--- a/paper-server/src/main/java/io/papermc/paper/registry/PaperRegistries.java
+++ b/paper-server/src/main/java/io/papermc/paper/registry/PaperRegistries.java
@@ -5,10 +5,16 @@
 import io.papermc.paper.datacomponent.DataComponentTypes;
 import io.papermc.paper.datacomponent.PaperDataComponentType;
 import io.papermc.paper.registry.data.PaperBannerPatternRegistryEntry;
+import io.papermc.paper.registry.data.PaperCatTypeRegistryEntry;
+import io.papermc.paper.registry.data.PaperChickenVariantRegistryEntry;
+import io.papermc.paper.registry.data.PaperCowVariantRegistryEntry;
 import io.papermc.paper.registry.data.PaperDamageTypeRegistryEntry;
 import io.papermc.paper.registry.data.PaperEnchantmentRegistryEntry;
+import io.papermc.paper.registry.data.PaperFrogVariantRegistryEntry;
 import io.papermc.paper.registry.data.PaperGameEventRegistryEntry;
 import io.papermc.paper.registry.data.PaperPaintingVariantRegistryEntry;
+import io.papermc.paper.registry.data.PaperPigVariantRegistryEntry;
+import io.papermc.paper.registry.data.PaperWolfVariantRegistryEntry;
 import io.papermc.paper.registry.entry.RegistryEntry;
 import io.papermc.paper.registry.entry.RegistryEntryMeta;
 import io.papermc.paper.registry.tag.TagKey;
@@ -111,18 +117,18 @@ public final class PaperRegistries {
             start(Registries.TRIM_MATERIAL, RegistryKey.TRIM_MATERIAL).craft(TrimMaterial.class, CraftTrimMaterial::new, true).build().delayed(),
             start(Registries.TRIM_PATTERN, RegistryKey.TRIM_PATTERN).craft(TrimPattern.class, CraftTrimPattern::new, true).build().delayed(),
             start(Registries.DAMAGE_TYPE, RegistryKey.DAMAGE_TYPE).craft(DamageType.class, CraftDamageType::new).writable(PaperDamageTypeRegistryEntry.PaperBuilder::new).delayed(),
-            start(Registries.WOLF_VARIANT, RegistryKey.WOLF_VARIANT).craft(Wolf.Variant.class, CraftWolf.CraftVariant::new).build().delayed(),
+            start(Registries.WOLF_VARIANT, RegistryKey.WOLF_VARIANT).craft(Wolf.Variant.class, CraftWolf.CraftVariant::new).writable(PaperWolfVariantRegistryEntry.PaperBuilder::new).delayed(),
             start(Registries.WOLF_SOUND_VARIANT, RegistryKey.WOLF_SOUND_VARIANT).craft(Wolf.SoundVariant.class, CraftWolf.CraftSoundVariant::new).build(),
             start(Registries.ENCHANTMENT, RegistryKey.ENCHANTMENT).craft(Enchantment.class, CraftEnchantment::new).serializationUpdater(FieldRename.ENCHANTMENT_RENAME).writable(PaperEnchantmentRegistryEntry.PaperBuilder::new).delayed(),
             start(Registries.JUKEBOX_SONG, RegistryKey.JUKEBOX_SONG).craft(JukeboxSong.class, CraftJukeboxSong::new).build().delayed(),
             start(Registries.BANNER_PATTERN, RegistryKey.BANNER_PATTERN).craft(PatternType.class, CraftPatternType::new, true).writable(PaperBannerPatternRegistryEntry.PaperBuilder::new).delayed(),
             start(Registries.PAINTING_VARIANT, RegistryKey.PAINTING_VARIANT).craft(Art.class, CraftArt::new, true).writable(PaperPaintingVariantRegistryEntry.PaperBuilder::new).delayed(),
             start(Registries.INSTRUMENT, RegistryKey.INSTRUMENT).craft(MusicInstrument.class, CraftMusicInstrument::new, true).build().delayed(),
-            start(Registries.CAT_VARIANT, RegistryKey.CAT_VARIANT).craft(Cat.Type.class, CraftCat.CraftType::new).build().delayed(),
-            start(Registries.FROG_VARIANT, RegistryKey.FROG_VARIANT).craft(Frog.Variant.class, CraftFrog.CraftVariant::new).build().delayed(),
-            start(Registries.CHICKEN_VARIANT, RegistryKey.CHICKEN_VARIANT).craft(Chicken.Variant.class, CraftChicken.CraftVariant::new).build(),
-            start(Registries.COW_VARIANT, RegistryKey.COW_VARIANT).craft(Cow.Variant.class, CraftCow.CraftVariant::new).build(),
-            start(Registries.PIG_VARIANT, RegistryKey.PIG_VARIANT).craft(Pig.Variant.class, CraftPig.CraftVariant::new).build(),
+            start(Registries.CAT_VARIANT, RegistryKey.CAT_VARIANT).craft(Cat.Type.class, CraftCat.CraftType::new).writable(PaperCatTypeRegistryEntry.PaperBuilder::new).delayed(),
+            start(Registries.FROG_VARIANT, RegistryKey.FROG_VARIANT).craft(Frog.Variant.class, CraftFrog.CraftVariant::new).writable(PaperFrogVariantRegistryEntry.PaperBuilder::new).delayed(),
+            start(Registries.CHICKEN_VARIANT, RegistryKey.CHICKEN_VARIANT).craft(Chicken.Variant.class, CraftChicken.CraftVariant::new).writable(PaperChickenVariantRegistryEntry.PaperBuilder::new),
+            start(Registries.COW_VARIANT, RegistryKey.COW_VARIANT).craft(Cow.Variant.class, CraftCow.CraftVariant::new).writable(PaperCowVariantRegistryEntry.PaperBuilder::new),
+            start(Registries.PIG_VARIANT, RegistryKey.PIG_VARIANT).craft(Pig.Variant.class, CraftPig.CraftVariant::new).writable(PaperPigVariantRegistryEntry.PaperBuilder::new),
 
             // api-only
             start(Registries.ENTITY_TYPE, RegistryKey.ENTITY_TYPE).apiOnly(PaperSimpleRegistry::entityType),
diff --git a/paper-server/src/main/java/io/papermc/paper/registry/data/PaperCatTypeRegistryEntry.java b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperCatTypeRegistryEntry.java
new file mode 100644
index 000000000000..c82d9bdfc041
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperCatTypeRegistryEntry.java
@@ -0,0 +1,60 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.PaperRegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import io.papermc.paper.registry.data.util.Conversions;
+import net.minecraft.world.entity.animal.CatVariant;
+import net.minecraft.world.entity.variant.SpawnPrioritySelectors;
+import org.bukkit.entity.Cat;
+import org.jspecify.annotations.Nullable;
+
+import static io.papermc.paper.registry.data.util.Checks.asArgument;
+import static io.papermc.paper.registry.data.util.Checks.asConfigured;
+
+public class PaperCatTypeRegistryEntry implements CatTypeRegistryEntry {
+
+    protected net.minecraft.core.@Nullable ClientAsset clientTextureAsset;
+    protected SpawnPrioritySelectors spawnConditions;
+
+    protected final Conversions conversions;
+
+    public PaperCatTypeRegistryEntry(
+        final Conversions conversions,
+        final @Nullable CatVariant internal
+    ) {
+        this.conversions = conversions;
+        if (internal == null) {
+            this.spawnConditions = SpawnPrioritySelectors.EMPTY;
+            return;
+        }
+
+        this.clientTextureAsset = internal.assetInfo();
+        this.spawnConditions = internal.spawnConditions();
+    }
+
+    @Override
+    public ClientTextureAsset clientTextureAsset() {
+        return this.conversions.asBukkit(asConfigured(this.clientTextureAsset, "clientTextureAsset"));
+    }
+
+    public static final class PaperBuilder extends PaperCatTypeRegistryEntry implements Builder, PaperRegistryBuilder<CatVariant, Cat.Type> {
+
+        public PaperBuilder(final Conversions conversions, final @Nullable CatVariant internal) {
+            super(conversions, internal);
+        }
+
+        @Override
+        public Builder clientTextureAsset(final ClientTextureAsset clientTextureAsset) {
+            this.clientTextureAsset = this.conversions.asVanilla(asArgument(clientTextureAsset, "clientTextureAsset"));
+            return this;
+        }
+
+        @Override
+        public CatVariant build() {
+            return new CatVariant(
+                asConfigured(this.clientTextureAsset, "clientTextureAsset"),
+                asConfigured(this.spawnConditions, "spawnConditions")
+            );
+        }
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/registry/data/PaperChickenVariantRegistryEntry.java b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperChickenVariantRegistryEntry.java
new file mode 100644
index 000000000000..281dfe8e5b00
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperChickenVariantRegistryEntry.java
@@ -0,0 +1,80 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.PaperRegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import io.papermc.paper.registry.data.util.Conversions;
+import net.minecraft.world.entity.animal.ChickenVariant;
+import net.minecraft.world.entity.variant.ModelAndTexture;
+import net.minecraft.world.entity.variant.SpawnPrioritySelectors;
+import org.bukkit.entity.Chicken;
+import org.jspecify.annotations.Nullable;
+
+import static io.papermc.paper.registry.data.util.Checks.asArgument;
+import static io.papermc.paper.registry.data.util.Checks.asConfigured;
+
+public class PaperChickenVariantRegistryEntry implements ChickenVariantRegistryEntry {
+
+    protected ChickenVariant.@Nullable ModelType model;
+    protected net.minecraft.core.@Nullable ClientAsset clientTextureAsset;
+    protected SpawnPrioritySelectors spawnConditions;
+
+    protected final Conversions conversions;
+
+    public PaperChickenVariantRegistryEntry(
+        final Conversions conversions,
+        final @Nullable ChickenVariant internal
+    ) {
+        this.conversions = conversions;
+        if (internal == null) {
+            this.spawnConditions = SpawnPrioritySelectors.EMPTY;
+            return;
+        }
+
+        this.clientTextureAsset = internal.modelAndTexture().asset();
+        this.model = internal.modelAndTexture().model();
+        this.spawnConditions = internal.spawnConditions();
+    }
+
+    @Override
+    public ClientTextureAsset clientTextureAsset() {
+        return this.conversions.asBukkit(asConfigured(this.clientTextureAsset, "clientTextureAsset"));
+    }
+
+    @Override
+    public Model model() {
+        return switch (asConfigured(this.model, "model")) {
+            case NORMAL -> Model.NORMAL;
+            case COLD -> Model.COLD;
+        };
+    }
+
+    public static final class PaperBuilder extends PaperChickenVariantRegistryEntry implements Builder, PaperRegistryBuilder<ChickenVariant, Chicken.Variant> {
+
+        public PaperBuilder(final Conversions conversions, final @Nullable ChickenVariant internal) {
+            super(conversions, internal);
+        }
+
+        @Override
+        public Builder clientTextureAsset(final ClientTextureAsset clientTextureAsset) {
+            this.clientTextureAsset = this.conversions.asVanilla(asArgument(clientTextureAsset, "clientTextureAsset"));
+            return this;
+        }
+
+        @Override
+        public Builder model(final Model model) {
+            this.model = switch (asArgument(model, "model")) {
+                case NORMAL -> ChickenVariant.ModelType.NORMAL;
+                case COLD -> ChickenVariant.ModelType.COLD;
+            };
+            return this;
+        }
+
+        @Override
+        public ChickenVariant build() {
+            return new ChickenVariant(
+                new ModelAndTexture<>(asConfigured(this.model, "model"), asConfigured(this.clientTextureAsset, "clientTextureAsset")),
+                asConfigured(this.spawnConditions, "spawnConditions")
+            );
+        }
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/registry/data/PaperCowVariantRegistryEntry.java b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperCowVariantRegistryEntry.java
new file mode 100644
index 000000000000..25d5e3198f91
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperCowVariantRegistryEntry.java
@@ -0,0 +1,82 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.PaperRegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import io.papermc.paper.registry.data.util.Conversions;
+import net.minecraft.world.entity.animal.CowVariant;
+import net.minecraft.world.entity.variant.ModelAndTexture;
+import net.minecraft.world.entity.variant.SpawnPrioritySelectors;
+import org.bukkit.entity.Cow;
+import org.jspecify.annotations.Nullable;
+
+import static io.papermc.paper.registry.data.util.Checks.asArgument;
+import static io.papermc.paper.registry.data.util.Checks.asConfigured;
+
+public class PaperCowVariantRegistryEntry implements CowVariantRegistryEntry {
+
+    protected CowVariant.@Nullable ModelType model = null;
+    protected net.minecraft.core.@Nullable ClientAsset clientTextureAsset = null;
+    protected SpawnPrioritySelectors spawnConditions;
+
+    protected final Conversions conversions;
+
+    public PaperCowVariantRegistryEntry(
+        final Conversions conversions,
+        final @Nullable CowVariant internal
+    ) {
+        this.conversions = conversions;
+        if (internal == null) {
+            this.spawnConditions = SpawnPrioritySelectors.EMPTY;
+            return;
+        }
+
+        this.clientTextureAsset = internal.modelAndTexture().asset();
+        this.model = internal.modelAndTexture().model();
+        this.spawnConditions = internal.spawnConditions();
+    }
+
+    @Override
+    public ClientTextureAsset clientTextureAsset() {
+        return this.conversions.asBukkit(asConfigured(this.clientTextureAsset, "clientTextureAsset"));
+    }
+
+    @Override
+    public Model model() {
+        return switch (asConfigured(this.model, "model")) {
+            case NORMAL -> Model.NORMAL;
+            case COLD -> Model.COLD;
+            case WARM -> Model.WARM;
+        };
+    }
+
+    public static final class PaperBuilder extends PaperCowVariantRegistryEntry implements Builder, PaperRegistryBuilder<CowVariant, Cow.Variant> {
+
+        public PaperBuilder(final Conversions conversions, final @Nullable CowVariant internal) {
+            super(conversions, internal);
+        }
+
+        @Override
+        public Builder clientTextureAsset(final ClientTextureAsset clientTextureAsset) {
+            this.clientTextureAsset = this.conversions.asVanilla(asArgument(clientTextureAsset, "clientTextureAsset"));
+            return this;
+        }
+
+        @Override
+        public Builder model(final Model model) {
+            this.model = switch (asArgument(model, "model")) {
+                case NORMAL -> CowVariant.ModelType.NORMAL;
+                case COLD -> CowVariant.ModelType.COLD;
+                case WARM -> CowVariant.ModelType.WARM;
+            };
+            return this;
+        }
+
+        @Override
+        public CowVariant build() {
+            return new CowVariant(
+                new ModelAndTexture<>(asConfigured(this.model, "model"), asConfigured(this.clientTextureAsset, "clientTextureAsset")),
+                this.spawnConditions
+            );
+        }
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/registry/data/PaperFrogVariantRegistryEntry.java b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperFrogVariantRegistryEntry.java
new file mode 100644
index 000000000000..953beb789d28
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperFrogVariantRegistryEntry.java
@@ -0,0 +1,60 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.PaperRegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import io.papermc.paper.registry.data.util.Conversions;
+import net.minecraft.world.entity.animal.frog.FrogVariant;
+import net.minecraft.world.entity.variant.SpawnPrioritySelectors;
+import org.bukkit.entity.Frog;
+import org.jspecify.annotations.Nullable;
+
+import static io.papermc.paper.registry.data.util.Checks.asArgument;
+import static io.papermc.paper.registry.data.util.Checks.asConfigured;
+
+public class PaperFrogVariantRegistryEntry implements FrogVariantRegistryEntry {
+
+    protected net.minecraft.core.@Nullable ClientAsset clientTextureAsset;
+    protected SpawnPrioritySelectors spawnConditions;
+
+    protected final Conversions conversions;
+
+    public PaperFrogVariantRegistryEntry(
+        final Conversions conversions,
+        final @Nullable FrogVariant internal
+    ) {
+        this.conversions = conversions;
+        if (internal == null) {
+            spawnConditions = SpawnPrioritySelectors.EMPTY;
+            return;
+        }
+
+        this.clientTextureAsset = internal.assetInfo();
+        this.spawnConditions = internal.spawnConditions();
+    }
+
+    @Override
+    public ClientTextureAsset clientTextureAsset() {
+        return this.conversions.asBukkit(asConfigured(this.clientTextureAsset, "clientTextureAsset"));
+    }
+
+    public static final class PaperBuilder extends PaperFrogVariantRegistryEntry implements Builder, PaperRegistryBuilder<FrogVariant, Frog.Variant> {
+
+        public PaperBuilder(final Conversions conversions, final @Nullable FrogVariant internal) {
+            super(conversions, internal);
+        }
+
+        @Override
+        public Builder clientTextureAsset(final ClientTextureAsset clientTextureAsset) {
+            this.clientTextureAsset = this.conversions.asVanilla(asArgument(clientTextureAsset, "clientTextureAsset"));
+            return this;
+        }
+
+        @Override
+        public FrogVariant build() {
+            return new FrogVariant(
+                asConfigured(this.clientTextureAsset, "clientTextureAsset"),
+                asConfigured(this.spawnConditions, "spawnConditions")
+            );
+        }
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/registry/data/PaperPigVariantRegistryEntry.java b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperPigVariantRegistryEntry.java
new file mode 100644
index 000000000000..c7e6c68b7ccf
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperPigVariantRegistryEntry.java
@@ -0,0 +1,80 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.PaperRegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import io.papermc.paper.registry.data.util.Conversions;
+import net.minecraft.world.entity.animal.PigVariant;
+import net.minecraft.world.entity.variant.ModelAndTexture;
+import net.minecraft.world.entity.variant.SpawnPrioritySelectors;
+import org.bukkit.entity.Pig;
+import org.jspecify.annotations.Nullable;
+
+import static io.papermc.paper.registry.data.util.Checks.asArgument;
+import static io.papermc.paper.registry.data.util.Checks.asConfigured;
+
+public class PaperPigVariantRegistryEntry implements PigVariantRegistryEntry {
+
+    protected PigVariant.@Nullable ModelType model;
+    protected net.minecraft.core.@Nullable ClientAsset clientTextureAsset;
+    protected SpawnPrioritySelectors spawnConditions;
+
+    protected final Conversions conversions;
+
+    public PaperPigVariantRegistryEntry(
+        final Conversions conversions,
+        final @Nullable PigVariant internal
+    ) {
+        this.conversions = conversions;
+        if (internal == null) {
+            spawnConditions = SpawnPrioritySelectors.EMPTY;
+            return;
+        }
+
+        this.clientTextureAsset = internal.modelAndTexture().asset();
+        this.model = internal.modelAndTexture().model();
+        this.spawnConditions = internal.spawnConditions();
+    }
+
+    @Override
+    public ClientTextureAsset clientTextureAsset() {
+        return this.conversions.asBukkit(asConfigured(this.clientTextureAsset, "clientTextureAsset"));
+    }
+
+    @Override
+    public Model model() {
+        return switch (asConfigured(this.model, "model")) {
+            case NORMAL -> Model.NORMAL;
+            case COLD -> Model.COLD;
+        };
+    }
+
+    public static final class PaperBuilder extends PaperPigVariantRegistryEntry implements Builder, PaperRegistryBuilder<PigVariant, Pig.Variant> {
+
+        public PaperBuilder(final Conversions conversions, final @Nullable PigVariant internal) {
+            super(conversions, internal);
+        }
+
+        @Override
+        public Builder clientTextureAsset(final ClientTextureAsset clientTextureAsset) {
+            this.clientTextureAsset = this.conversions.asVanilla(asArgument(clientTextureAsset, "clientTextureAsset"));
+            return this;
+        }
+
+        @Override
+        public Builder model(final Model model) {
+            this.model = switch (asArgument(model, "model")) {
+                case NORMAL -> PigVariant.ModelType.NORMAL;
+                case COLD -> PigVariant.ModelType.COLD;
+            };
+            return this;
+        }
+
+        @Override
+        public PigVariant build() {
+            return new PigVariant(
+                new ModelAndTexture<>(asConfigured(this.model, "model"), asConfigured(this.clientTextureAsset, "clientTextureAsset")),
+                asConfigured(this.spawnConditions, "spawnConditions")
+            );
+        }
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/registry/data/PaperWolfVariantRegistryEntry.java b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperWolfVariantRegistryEntry.java
new file mode 100644
index 000000000000..ca14e9d622e8
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/registry/data/PaperWolfVariantRegistryEntry.java
@@ -0,0 +1,90 @@
+package io.papermc.paper.registry.data;
+
+import io.papermc.paper.registry.PaperRegistryBuilder;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
+import io.papermc.paper.registry.data.util.Conversions;
+import net.minecraft.world.entity.animal.wolf.WolfVariant;
+import net.minecraft.world.entity.variant.SpawnPrioritySelectors;
+import org.bukkit.entity.Wolf;
+import org.jspecify.annotations.Nullable;
+
+import static io.papermc.paper.registry.data.util.Checks.asArgument;
+import static io.papermc.paper.registry.data.util.Checks.asConfigured;
+
+public class PaperWolfVariantRegistryEntry implements WolfVariantRegistryEntry {
+
+    protected net.minecraft.core.@Nullable ClientAsset angryClientTextureAsset;
+    protected net.minecraft.core.@Nullable ClientAsset wildClientTextureAsset;
+    protected net.minecraft.core.@Nullable ClientAsset tameClientTextureAsset;
+    protected SpawnPrioritySelectors spawnConditions;
+
+    protected final Conversions conversions;
+
+    public PaperWolfVariantRegistryEntry(
+        final Conversions conversions,
+        final @Nullable WolfVariant internal
+    ) {
+        this.conversions = conversions;
+        if (internal == null) {
+            this.spawnConditions = SpawnPrioritySelectors.EMPTY;
+            return;
+        }
+
+        this.angryClientTextureAsset = internal.assetInfo().angry();
+        this.wildClientTextureAsset = internal.assetInfo().wild();
+        this.tameClientTextureAsset = internal.assetInfo().tame();
+        this.spawnConditions = internal.spawnConditions();
+    }
+
+    @Override
+    public ClientTextureAsset angryClientTextureAsset() {
+        return this.conversions.asBukkit(asConfigured(this.angryClientTextureAsset, "angryClientTextureAsset"));
+    }
+
+    @Override
+    public ClientTextureAsset wildClientTextureAsset() {
+        return this.conversions.asBukkit(asConfigured(this.wildClientTextureAsset, "wildClientTextureAsset"));
+    }
+
+    @Override
+    public ClientTextureAsset tameClientTextureAsset() {
+        return this.conversions.asBukkit(asConfigured(this.tameClientTextureAsset, "tameClientTextureAsset"));
+    }
+
+    public static final class PaperBuilder extends PaperWolfVariantRegistryEntry implements Builder, PaperRegistryBuilder<WolfVariant, Wolf.Variant> {
+
+        public PaperBuilder(final Conversions conversions, final @Nullable WolfVariant internal) {
+            super(conversions, internal);
+        }
+
+        @Override
+        public Builder angryClientTextureAsset(final ClientTextureAsset angryClientTextureAsset) {
+            this.angryClientTextureAsset = this.conversions.asVanilla(asArgument(angryClientTextureAsset, "angryClientTextureAsset"));
+            return this;
+        }
+
+        @Override
+        public Builder wildClientTextureAsset(final ClientTextureAsset wildClientTextureAsset) {
+            this.wildClientTextureAsset = this.conversions.asVanilla(asArgument(wildClientTextureAsset, "wildClientTextureAsset"));
+            return this;
+        }
+
+        @Override
+        public Builder tameClientTextureAsset(final ClientTextureAsset tameClientTextureAsset) {
+            this.tameClientTextureAsset = this.conversions.asVanilla(asArgument(tameClientTextureAsset, "tameClientTextureAsset"));
+            return this;
+        }
+
+        @Override
+        public WolfVariant build() {
+            return new WolfVariant(
+                new WolfVariant.AssetInfo(
+                    asConfigured(this.wildClientTextureAsset, "wildClientTextureAsset"),
+                    asConfigured(this.tameClientTextureAsset, "tameClientTextureAsset"),
+                    asConfigured(this.angryClientTextureAsset, "angryClientTextureAsset")
+                ),
+                asConfigured(this.spawnConditions, "spawnConditions")
+            );
+        }
+    }
+}
diff --git a/paper-server/src/main/java/io/papermc/paper/registry/data/util/Conversions.java b/paper-server/src/main/java/io/papermc/paper/registry/data/util/Conversions.java
index b1710da835ce..b6a9fd509a4f 100644
--- a/paper-server/src/main/java/io/papermc/paper/registry/data/util/Conversions.java
+++ b/paper-server/src/main/java/io/papermc/paper/registry/data/util/Conversions.java
@@ -2,8 +2,10 @@
 
 import com.google.common.base.Preconditions;
 import com.mojang.serialization.JavaOps;
+import io.papermc.paper.adventure.PaperAdventure;
 import io.papermc.paper.adventure.WrapperAwareSerializer;
 import java.util.Optional;
+import io.papermc.paper.registry.data.client.ClientTextureAsset;
 import net.kyori.adventure.text.Component;
 import net.minecraft.core.Registry;
 import net.minecraft.core.RegistryAccess;
@@ -33,7 +35,6 @@ public <T> Optional<RegistryOps.RegistryInfo<T>> lookup(final ResourceKey<? exte
         return globalInstance;
     }
 
-
     private final RegistryOps.RegistryInfoLookup lookup;
     private final WrapperAwareSerializer serializer;
 
@@ -55,4 +56,18 @@ public RegistryOps.RegistryInfoLookup lookup() {
     public Component asAdventure(final net.minecraft.network.chat.@Nullable Component vanilla) {
         return vanilla == null ? Component.empty() : this.serializer.deserialize(vanilla);
     }
+
+    public ClientTextureAsset asBukkit(final net.minecraft.core.@Nullable ClientAsset clientTextureAsset) {
+        return clientTextureAsset == null ? null : ClientTextureAsset.clientTextureAsset(
+            PaperAdventure.asAdventure(clientTextureAsset.id()),
+            PaperAdventure.asAdventure(clientTextureAsset.texturePath())
+        );
+    }
+
+    public net.minecraft.core.ClientAsset asVanilla(final @Nullable ClientTextureAsset clientTextureAsset) {
+        return clientTextureAsset == null ? null : new net.minecraft.core.ClientAsset(
+            PaperAdventure.asVanilla(clientTextureAsset.identifier()),
+            PaperAdventure.asVanilla(clientTextureAsset.texturePath())
+        );
+    }
 }


Reviews:
user: login: lynxplay
body: Glanced a bit more, this is not correctly tracking the fact that ClientAsset can have a different texture path from the id.
That either needs to be tracked internally or exposed to the API.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T21:23:39Z
user: login: masmc05
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:08:39Z
user: login: lynxplay
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:31:22Z
user: login: masmc05
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:36:01Z
user: login: lynxplay
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:39:47Z
user: login: lynxplay
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T20:48:02Z
user: login: Owen1212055
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-06T20:49:02Z

Review comments:
diff_hunk: @@ -59,6 +62,11 @@ public interface Chicken extends Animals {
      */
     interface Variant extends Keyed {
 
+        enum Model {
path: paper-api/src/main/java/org/bukkit/entity/Chicken.java
user: login: lynxplay
body: I am a bit iffy on having the model be defined here.
Given those are very *very* version specific, I think I'd prefer them on the registry entries.

In general, I am unsure how useful exposing the assetId and model of a variant truely is to the *general* server API. I don't *really* have a usecase in mind for it, if you have one please share :)
Otherwise, I think we'd be best of just not exposing these in the stable API.
created_at: 2025-04-26T16:08:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +49,20 @@ interface Variant extends Keyed {
         Variant WARM = getVariant("warm");
         // End generate - CowVariant
 
+        /**
+         * Get the cow variant's asset id
+         *
+         * @return the asset id
+         */
+        @NotNull Key assetId();
+
+        /**
+         * Get the cow variant's {@link Cow.Variant.Model}. Defaults to {@link Cow.Variant.Model#NORMAL}.
+         *
+         * @return the model
+         */
+        @Nullable Model getModel();
path: paper-api/src/main/java/org/bukkit/entity/Cow.java
user: login: lynxplay
body: Same here as on chicken.
created_at: 2025-04-26T16:08:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,6 +62,13 @@ interface Variant extends OldEnum<Variant>, Keyed {
         Variant WARM = getVariant("warm");
         // End generate - FrogVariant
 
+        /**
+         * Get the frog variant's asset id
+         *
+         * @return the asset id
+         */
+        @NotNull Key assetId();
path: paper-api/src/main/java/org/bukkit/entity/Frog.java
user: login: lynxplay
body: Same here as on chicken.
created_at: 2025-04-26T16:08:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +34,11 @@ public interface Pig extends Steerable, Vehicle {
      */
     interface Variant extends Keyed {
 
+        enum Model {
path: paper-api/src/main/java/org/bukkit/entity/Pig.java
user: login: lynxplay
body: Same here as on chicken.
created_at: 2025-04-26T16:08:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +132,27 @@ interface Variant extends Keyed {
         Variant WOODS = getVariant("woods");
         // End generate - WolfVariant
 
+        /**
+         * Get the wolf variant's asset id used when the wolf is angry
+         *
+         * @return the asset id
+         */
+        @NotNull Key assetIdAngry();
path: paper-api/src/main/java/org/bukkit/entity/Wolf.java
user: login: lynxplay
body: Same here as on chicken.
created_at: 2025-04-26T16:08:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package io.papermc.paper.registry.data.client;
+
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.key.KeyPattern;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * A data-centric version-specific representation of a client asset, composed of an identifier and a path.
+ * Follows the same, version-specific, compatibility guarantees as the RegistryEntry types it is used in.
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface ClientAsset {
path: paper-api/src/main/java/io/papermc/paper/registry/data/client/ClientAsset.java
user: login: masmc05
body: I feel like something like `TextureAsset` would be a better name, as the api already uses other client assets like item models, which aren't represented by/with this.
created_at: 2025-05-05T20:08:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package io.papermc.paper.registry.data.client;
+
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.key.KeyPattern;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * A data-centric version-specific representation of a client asset, composed of an identifier and a path.
+ * Follows the same, version-specific, compatibility guarantees as the RegistryEntry types it is used in.
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface ClientAsset {
path: paper-api/src/main/java/io/papermc/paper/registry/data/client/ClientAsset.java
user: login: lynxplay
body: Hmmm, yea *maybe*. ClientTextureAsset?
I agree it is a bit weird that some things are only represented via a Key, others by this new type, but that is kinda how internals go and the registry data API is pretty much an API mirror of those.
created_at: 2025-05-05T20:31:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package io.papermc.paper.registry.data.client;
+
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.key.KeyPattern;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * A data-centric version-specific representation of a client asset, composed of an identifier and a path.
+ * Follows the same, version-specific, compatibility guarantees as the RegistryEntry types it is used in.
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface ClientAsset {
path: paper-api/src/main/java/io/papermc/paper/registry/data/client/ClientAsset.java
user: login: masmc05
body: ClientTextureAsset sounds good
> but that is kinda how internals go

Yeah, ik, still I consider copy pasting non-datapack nms names that are confusing in context of the api isn't that good
created_at: 2025-05-05T20:36:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+package io.papermc.paper.registry.data.client;
+
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.key.KeyPattern;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * A data-centric version-specific representation of a client asset, composed of an identifier and a path.
+ * Follows the same, version-specific, compatibility guarantees as the RegistryEntry types it is used in.
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface ClientAsset {
path: paper-api/src/main/java/io/papermc/paper/registry/data/client/ClientAsset.java
user: login: lynxplay
body: Yea, I guess the method already includes "texture" in there.
If things change down the line, we can rename it anyway as this isn't stable API.
created_at: 2025-05-05T20:39:47Z
author_association: CONTRIBUTOR

PR comments:
user: login: BertTowne
created_at: 2025-04-22T16:21:13Z
body: This should build/test successfully now. Internal mob variants should not have had `SpawnPrioritySelectors.EMPTY`
author_association: CONTRIBUTOR
user: login: lynxplay
created_at: 2025-05-03T21:24:21Z
body: FWIW, I have diff fixing a lot of this locally. I'll push that tomorrow before we duplicate work here.
author_association: CONTRIBUTOR
user: login: BertTowne
created_at: 2025-05-04T05:27:52Z
body: Sounds good, thank you! I didn't see the comments until I got the notif for this, but I do not have any use in mind for exposing that additional variant info to the general API - I more so added that for completionism. Leaving it in the registry entries makes sense to me.
author_association: CONTRIBUTOR
user: login: lynxplay
created_at: 2025-05-05T19:19:09Z
body: Pushed over my changes, will review down the line. Feel free to also review my changes, hope I didn't miss anything.

![image](https://github.com/user-attachments/assets/11e28bf2-7ab2-43e8-86d1-03aec7677161)

author_association: CONTRIBUTOR
user: login: BertTowne
created_at: 2025-05-05T21:28:36Z
body: I like the changes. `ClientTextureAsset` is much cleaner. I do feel kinda bad about not touching SpawnConditions at all, but I would have no idea how to elegantly implement that in a way that can not only be used by these custom mob variants, but possibly other mobs through a custom SpawnCondition api.
author_association: CONTRIBUTOR
user: login: lynxplay
created_at: 2025-05-05T21:30:05Z
body: Generally, it is perfectly fine like this and if/once a usecase for spawn conditions is raised by someone, they can PR it :+1:
author_association: CONTRIBUTOR