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." +}