diff --git a/README.md b/README.md
index 160f1b75..bd37b867 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ Just choose a version and use its version number.
| `level_events` | Provides common level events for mods. |
| `loot` | A small library to modify mob loot |
| `mixin_extensions` | More features for Mixins |
+| `model_data` | Addon to model api to make building model data easier. |
| `model_loader` | Base loader for custom model types |
| `models` | Model implementations, ModelData, RenderTypes |
| `obj_loader` | Loading .obj models |
diff --git a/modules/model_data/build.gradle b/modules/model_data/build.gradle
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/modules/model_data/build.gradle
@@ -0,0 +1 @@
+
diff --git a/modules/model_data/src/main/java/io/github/fabricators_of_create/porting_lib/models/data/ModelData.java b/modules/model_data/src/main/java/io/github/fabricators_of_create/porting_lib/models/data/ModelData.java
new file mode 100644
index 00000000..75d3dbc3
--- /dev/null
+++ b/modules/model_data/src/main/java/io/github/fabricators_of_create/porting_lib/models/data/ModelData.java
@@ -0,0 +1,119 @@
+package io.github.fabricators_of_create.porting_lib.models.data;
+
+import com.google.common.base.Preconditions;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceArrayMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.resources.model.BakedModel;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.util.RandomSource;
+import net.minecraft.world.level.BlockAndTintGetter;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * A container for data to be passed to {@link BakedModel} instances.
+ *
+ * All objects stored in here MUST BE IMMUTABLE OR THREAD-SAFE.
+ * Properties will be accessed from another thread.
+ *
+ * @see ModelProperty
+ * @see BlockEntity#getRenderData()
+ * @see BakedModel#getQuads(BlockState, Direction, RandomSource)
+ */
+public final class ModelData {
+ public static final ModelData EMPTY = ModelData.builder().build();
+
+ private final Map, Object> properties;
+
+ @Nullable
+ private Set> propertySetView;
+
+ private ModelData(Map, Object> properties) {
+ this.properties = properties;
+ }
+
+ public Set> getProperties() {
+ var view = propertySetView;
+ if (view == null) {
+ propertySetView = view = Collections.unmodifiableSet(properties.keySet());
+ }
+ return view;
+ }
+
+ public boolean has(ModelProperty> property) {
+ return properties.containsKey(property);
+ }
+
+ @Nullable
+ public T get(ModelProperty property) {
+ return (T) properties.get(property);
+ }
+
+ public Builder derive() {
+ return new Builder(this);
+ }
+
+ public static Builder builder() {
+ return new Builder(null);
+ }
+
+ /**
+ * Helper to create a {@link ModelData} instance for a single property-value pair, without the verbosity
+ * and runtime overhead of creating a builder object.
+ */
+ public static ModelData of(ModelProperty property, T value) {
+ Preconditions.checkState(property.test(value), "The provided value is invalid for this property.");
+ // Must use one of the two map types from the builder to avoid megamorphic calls to Map.get() later
+ Reference2ReferenceArrayMap, Object> map = new Reference2ReferenceArrayMap<>(1);
+ map.put(property, value);
+ return new ModelData(map);
+ }
+
+ public static final class Builder {
+ /**
+ * Hash maps are slower than array maps for *extremely* small maps (empty maps or singletons are the most
+ * extreme examples). Many block entities/models only use a single model data property, which means the
+ * overhead of hashing is quite wasteful. However, we do want to support any number of properties with
+ * reasonable performance. Therefore, we use an array map until the number of properties reaches this
+ * threshold, at which point we convert it to a hash map.
+ */
+ private static final int HASH_THRESHOLD = 4;
+
+ private Map, Object> properties;
+
+ private Builder(@Nullable ModelData parent) {
+ if (parent != null) {
+ // When cloning the map, use the expected type based on size
+ properties = parent.properties.size() >= HASH_THRESHOLD ? new Reference2ReferenceOpenHashMap<>(parent.properties) : new Reference2ReferenceArrayMap<>(parent.properties);
+ } else {
+ // Allocate the maximum number of entries we'd ever put into the map.
+ // We convert to a hash map *after* insertion of the HASH_THRESHOLD
+ // entry, so we need at least that many spots.
+ properties = new Reference2ReferenceArrayMap<>(HASH_THRESHOLD);
+ }
+ }
+
+ @Contract("_, _ -> this")
+ public Builder with(ModelProperty property, T value) {
+ Preconditions.checkState(property.test(value), "The provided value is invalid for this property.");
+ properties.put(property, value);
+ // Convert to a hash map if needed
+ if (properties.size() == HASH_THRESHOLD && properties instanceof Reference2ReferenceArrayMap, Object>) {
+ properties = new Reference2ReferenceOpenHashMap<>(properties);
+ }
+ return this;
+ }
+
+ @Contract("-> new")
+ public ModelData build() {
+ return new ModelData(properties);
+ }
+ }
+}
diff --git a/modules/model_data/src/main/java/io/github/fabricators_of_create/porting_lib/models/data/ModelProperty.java b/modules/model_data/src/main/java/io/github/fabricators_of_create/porting_lib/models/data/ModelProperty.java
new file mode 100644
index 00000000..1dda4c27
--- /dev/null
+++ b/modules/model_data/src/main/java/io/github/fabricators_of_create/porting_lib/models/data/ModelProperty.java
@@ -0,0 +1,28 @@
+package io.github.fabricators_of_create.porting_lib.models.data;
+
+import com.google.common.base.Predicates;
+import java.util.function.Predicate;
+
+/**
+ * A property to be used in {@link ModelData}.
+ *
+ * May optionally validate incoming values.
+ *
+ * @see ModelData
+ */
+public class ModelProperty implements Predicate {
+ private final Predicate predicate;
+
+ public ModelProperty() {
+ this(Predicates.alwaysTrue());
+ }
+
+ public ModelProperty(Predicate predicate) {
+ this.predicate = predicate;
+ }
+
+ @Override
+ public boolean test(T value) {
+ return predicate.test(value);
+ }
+}
diff --git a/modules/model_data/src/main/resources/fabric.mod.json b/modules/model_data/src/main/resources/fabric.mod.json
new file mode 100644
index 00000000..1ebcf684
--- /dev/null
+++ b/modules/model_data/src/main/resources/fabric.mod.json
@@ -0,0 +1,8 @@
+
+{
+ "schemaVersion": 1,
+ "id": "porting_lib_model_data",
+ "version": "${version}",
+ "name": "Porting Lib Model Data",
+ "description": "Addon to model api to make building model data easier."
+}