diff --git a/patches/minecraft/net/minecraft/client/renderer/block/model/ModelBlockDefinition.java.patch b/patches/minecraft/net/minecraft/client/renderer/block/model/ModelBlockDefinition.java.patch new file mode 100644 index 000000000..0e859a434 --- /dev/null +++ b/patches/minecraft/net/minecraft/client/renderer/block/model/ModelBlockDefinition.java.patch @@ -0,0 +1,11 @@ +--- ../src-base/minecraft/net/minecraft/client/renderer/block/model/ModelBlockDefinition.java ++++ ../src-work/minecraft/net/minecraft/client/renderer/block/model/ModelBlockDefinition.java +@@ -32,7 +32,7 @@ + + public static ModelBlockDefinition func_178331_a(Reader p_178331_0_) + { +- return (ModelBlockDefinition)field_178333_a.fromJson(p_178331_0_, ModelBlockDefinition.class); ++ return net.minecraftforge.client.model.BlockStateLoader.load(p_178331_0_, field_178333_a); + } + + public ModelBlockDefinition(Collection p_i46221_1_) diff --git a/src/main/java/net/minecraftforge/client/model/BlockStateLoader.java b/src/main/java/net/minecraftforge/client/model/BlockStateLoader.java new file mode 100644 index 000000000..015372da3 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/BlockStateLoader.java @@ -0,0 +1,205 @@ +package net.minecraftforge.client.model; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.vecmath.Matrix4f; + +import net.minecraft.client.renderer.block.model.ModelBlockDefinition; +import net.minecraft.client.resources.model.ModelRotation; +import net.minecraft.util.ResourceLocation; + +import org.apache.commons.io.Charsets; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +@SuppressWarnings("deprecation") +public class BlockStateLoader +{ + private static final Gson GSON = (new GsonBuilder()) + .registerTypeAdapter(ForgeBlockStateV1.class, ForgeBlockStateV1.Deserializer.INSTANCE) + .registerTypeAdapter(ForgeBlockStateV1.Variant.class, ForgeBlockStateV1.Variant.Deserializer.INSTANCE) + .create(); + /** + * Loads a BlockStates json file. + * Will attempt to parse it as a Forge Enhanced version if possible. + * Will fall back to standard loading if marker is not present. + * + * Note: This method is NOT thread safe + * + * @param reader json read + * @param vanillaGSON ModelBlockDefinition's GSON reader. + * + * @return Model definition including variants for all known combinations. + */ + @SuppressWarnings("rawtypes") + public static ModelBlockDefinition load(Reader reader, final Gson vanillaGSON) + { + try + { + byte[] data = IOUtils.toByteArray(reader); + reader = new InputStreamReader(new ByteArrayInputStream(data), Charsets.UTF_8); + + Marker marker = GSON.fromJson(new String(data), Marker.class); // Read "forge_marker" to determine what to load. + + switch (marker.forge_marker) + { + case 1: // Version 1 + ForgeBlockStateV1 v1 = GSON.fromJson(reader, ForgeBlockStateV1.class); + List variants = Lists.newArrayList(); + + for (Entry> entry : v1.variants.asMap().entrySet()) + { // Convert Version1 variants into vanilla variants for the ModelBlockDefinition. + List mcVars = Lists.newArrayList(); + for (ForgeBlockStateV1.Variant var : entry.getValue()) + { + ModelRotation rot = var.getRotation().or(ModelRotation.X0_Y0); + boolean uvLock = var.getUvLock().or(false); + int weight = var.getWeight().or(1); + + if (var.getModel() != null && var.getSubmodels().size() == 0 && var.getTextures().size() == 0) + mcVars.add(new ModelBlockDefinition.Variant(var.getModel(), rot, uvLock, weight)); + else + mcVars.add(new ForgeVariant(var.getModel(), rot, uvLock, weight, var.getTextures(), var.getOnlyPartsVariant(), var.getCustomData())); + } + variants.add(new ModelBlockDefinition.Variants(entry.getKey(), mcVars)); + } + + return new ModelBlockDefinition((Collection)variants); //Damn lists being collections! + + default: //Unknown version.. try loading it as normal. + return vanillaGSON.fromJson(reader, ModelBlockDefinition.class); + } + } + catch (IOException e) + { + Throwables.propagate(e); + } + return null; + } + + public static class Marker + { + public int forge_marker = -1; + } + + //This is here specifically so that we do not have a hard reference to ForgeBlockStateV1.Variant in ForgeVariant + public static class SubModel + { + private final ModelRotation rotation; + private final boolean uvLock; + private final ImmutableMap textures; + private final ResourceLocation model; + private final ImmutableMap customData; + + public SubModel(ModelRotation rotation, boolean uvLock, ImmutableMap textures, ResourceLocation model, ImmutableMap customData) + { + this.rotation = rotation; + this.uvLock = uvLock; + this.textures = textures; + this.model = model; + this.customData = customData; + } + + public ModelRotation getRotation() { return rotation; } + public boolean isUVLock() { return uvLock; } + public ImmutableMap getTextures() { return textures; } + public ResourceLocation getModelLocation() { return model; } + public ImmutableMap getCustomData() { return customData; } + } + + private static class ForgeVariant extends ModelBlockDefinition.Variant implements ISmartVariant + { + private final ImmutableMap textures; + private final ImmutableMap parts; + private final ImmutableMap customData; + + public ForgeVariant(ResourceLocation model, ModelRotation rotation, boolean uvLock, int weight, ImmutableMap textures, ImmutableMap parts, ImmutableMap customData) + { + super(model == null ? new ResourceLocation("builtin/missing") : model, rotation, uvLock, weight); + this.textures = textures; + this.parts = parts; + this.customData = customData; + } + + protected IModel runModelHooks(IModel base, ImmutableMap textureMap, ImmutableMap customData) + { + if (!customData.isEmpty()) + { + if (base instanceof IModelCustomData) + base = ((IModelCustomData)base).process(customData); + else + throw new RuntimeException("Attempted to add custom data to a model that doesn't need it: " + base); + } + + if (!textureMap.isEmpty()) + { + if (base instanceof IRetexturableModel) + base = ((IRetexturableModel)base).retexture(textureMap); + else + throw new RuntimeException("Attempted to retexture a non-retexturable model: " + base); + } + + return base; + } + + /** + * Used to replace the base model with a retextured model containing submodels. + */ + @Override + public IModel process(IModel base, ModelLoader loader) + { + int size = parts.size(); + boolean hasBase = base != loader.getMissingModel(); + + if (hasBase) + { + base = runModelHooks(base, textures, customData); + + if (size <= 0) + return base; + } + + // Apply rotation of base model to submodels. + // If baseRot is non-null, then that rotation will be applied instead of the base model's rotation. + // This is used to allow replacing base model with a submodel when there is no base model for a variant. + ModelRotation baseRot = getRotation(); + ImmutableMap.Builder> models = ImmutableMap.builder(); + for (Entry entry : parts.entrySet()) + { + SubModel part = entry.getValue(); + + Matrix4f matrix = new Matrix4f(baseRot.getMatrix()); + matrix.mul(part.getRotation().getMatrix()); + IModelState partState = new TRSRTransformation(matrix); + if (part.isUVLock()) partState = new ModelLoader.UVLock(partState); + + models.put(entry.getKey(), Pair.of(runModelHooks(loader.getModel(part.getModelLocation()), part.getTextures(), part.getCustomData()), partState)); + } + + return new MultiModel(hasBase ? base : null, baseRot, models.build()); + } + + @Override + public String toString() + { + StringBuilder buf = new StringBuilder(); + buf.append("TexturedVariant:"); + for (Entry e: this.textures.entrySet()) + buf.append(" ").append(e.getKey()).append(" = ").append(e.getValue()); + return buf.toString(); + } + } +} diff --git a/src/main/java/net/minecraftforge/client/model/ForgeBlockStateV1.java b/src/main/java/net/minecraftforge/client/model/ForgeBlockStateV1.java new file mode 100644 index 000000000..c57515b92 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/ForgeBlockStateV1.java @@ -0,0 +1,519 @@ +package net.minecraftforge.client.model; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import net.minecraft.client.resources.model.ModelRotation; +import net.minecraft.util.JsonUtils; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.client.model.BlockStateLoader.SubModel; +import net.minecraftforge.client.model.BlockStateLoader.Marker; +import net.minecraftforge.fml.common.FMLLog; + +import com.google.common.base.Optional; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +public class ForgeBlockStateV1 extends Marker +{ + ForgeBlockStateV1.Variant defaults; + Multimap variants = HashMultimap.create(); + + public static class Deserializer implements JsonDeserializer + { + static ForgeBlockStateV1.Deserializer INSTANCE = new Deserializer(); + @Override + public ForgeBlockStateV1 deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException + { + JsonObject json = element.getAsJsonObject(); + ForgeBlockStateV1 ret = new ForgeBlockStateV1(); + ret.forge_marker = JsonUtils.getJsonObjectIntegerFieldValue(json, "forge_marker"); + + if (json.has("defaults")) // Load defaults Variant. + { + ret.defaults = context.deserialize(json.get("defaults"), ForgeBlockStateV1.Variant.class); + + if (ret.defaults.simpleSubmodels.size() > 0) + throw new RuntimeException("\"defaults\" variant cannot contain a simple \"submodel\" definition."); + } + + Map> condensed = Maps.newHashMap(); // map(property name -> map(property value -> variant)) + Multimap specified = HashMultimap.create(); // Multimap containing all the states specified with "property=value". + + for (Entry e : JsonUtils.getJsonObject(json, "variants").entrySet()) + { + if (e.getKey().contains("=")) //Normal fully defined variant + { + if (e.getValue().isJsonArray()) + { + for (JsonElement a : e.getValue().getAsJsonArray()) + { + Variant.Deserializer.INSTANCE.simpleSubmodelKey = e.getKey(); + specified.put(e.getKey(), (ForgeBlockStateV1.Variant)context.deserialize(a, ForgeBlockStateV1.Variant.class)); + } + } + else + { + Variant.Deserializer.INSTANCE.simpleSubmodelKey = e.getKey(); + specified.put(e.getKey(), (ForgeBlockStateV1.Variant)context.deserialize(e.getValue(), ForgeBlockStateV1.Variant.class)); + } + } + else + { + Map subs = Maps.newHashMap(); + condensed.put(e.getKey(), subs); + for (Entry se : e.getValue().getAsJsonObject().entrySet()) + { + Variant.Deserializer.INSTANCE.simpleSubmodelKey = e.getKey() + "=" + se.getKey(); + subs.put(se.getKey(), (ForgeBlockStateV1.Variant)context.deserialize(se.getValue(), ForgeBlockStateV1.Variant.class)); + } + } + } + + Multimap permutations = getPermutations(condensed); // Get permutations of Forge-style states. + + for (Entry> e : specified.asMap().entrySet()) + { // Make fully-specified variants override Forge variant permutations, inheriting the permutations' values. + Collection baseVars = permutations.get(e.getKey()); + List addVars = Lists.newArrayList(); + + for (ForgeBlockStateV1.Variant specVar : e.getValue()) + { + if (!baseVars.isEmpty()) + { + for (ForgeBlockStateV1.Variant baseVar : baseVars) + addVars.add(new Variant(specVar).sync(baseVar)); + } + else + addVars.add(specVar); + } + + baseVars.clear(); + baseVars.addAll(addVars); + } + + for (Entry e : permutations.entries()) // Create the output map(state -> Variant). + { + ForgeBlockStateV1.Variant v = e.getValue(); + + if (ret.defaults != null) + { + v.sync(ret.defaults); // Sync defaults into all permutation variants + + for (Entry partKey : v.simpleSubmodels.entrySet()) + { // Sync variant values (including defaults) into simple submodel declarations. + if (partKey.getValue() == null) + continue; + + if (!v.submodels.containsKey(partKey.getKey())) + throw new RuntimeException("This should never happen! Simple submodel is not contained in the submodel map!"); + List partList = v.submodels.get(partKey.getKey()); + if (partList.size() > 1) + throw new RuntimeException("This should never happen! Simple submodel has multiple variants!"); + + ForgeBlockStateV1.Variant part = partList.get(0); + // Must keep old rotation for the part, because the base variant's rotation is applied to the parts already. + Optional rotation = part.rotation; + part.sync(v); + part.simpleSubmodels.clear(); + part.rotation = rotation; + } + } + + if (v.textures != null) + { + for (Entry tex : v.textures.entrySet()) + { + if (tex.getValue() != null && tex.getValue().charAt(0) == '#') + { + String value = v.textures.get(tex.getValue().substring(1)); + if (value == null) + { + FMLLog.severe("Could not resolve texture name \"" + tex.getValue() + "\" for permutation \"" + e.getKey() + "\""); + for (Entry t: v.textures.entrySet()) + FMLLog.severe(t.getKey() + "=" + t.getValue()); + throw new JsonParseException("Could not resolve texture name \"" + tex.getValue() + "\" for permutation \"" + e.getKey() + "\""); + } + v.textures.put(tex.getKey(), value); + } + } + + for (List part : v.submodels.values()) // Sync base variant's textures (including defaults) into all submodels. + { + for (ForgeBlockStateV1.Variant partVar : part) + { + for (Entry texEntry : v.textures.entrySet()) + { + if (!partVar.textures.containsKey(texEntry.getKey())) + partVar.textures.put(texEntry.getKey(), texEntry.getValue()); + } + } + } + } + + if (!v.submodels.isEmpty()) + ret.variants.putAll(e.getKey(), getSubmodelPermutations(v, v.submodels)); // Do permutations of submodel variants. + else + ret.variants.put(e.getKey(), v); + } + + return ret; + } + + private Multimap getPermutations(List sorted, Map> base, int depth, String prefix, Multimap ret, ForgeBlockStateV1.Variant parent) + { + if (depth == sorted.size()) + { + ret.put(prefix, parent); + return ret; + } + + String name = sorted.get(depth); + for (Entry e : base.get(name).entrySet()) + { + ForgeBlockStateV1.Variant newHead = parent == null ? new Variant(e.getValue()) : new Variant(parent).sync(e.getValue()); + + getPermutations(sorted, base, depth + 1, prefix + (depth == 0 ? "" : ",") + name + "=" + e.getKey(), ret, newHead); + } + + return ret; + } + + private Multimap getPermutations(Map> base) + { + List sorted = Lists.newArrayList(base.keySet()); + Collections.sort(sorted); // Sort to get consistent results. + return getPermutations(sorted, base, 0, "", HashMultimap.create(), null); + } + + private List getSubmodelPermutations(ForgeBlockStateV1.Variant baseVar, List sorted, Map> map, int depth, Map parts, List ret) + { + if (depth >= sorted.size()) + { + ForgeBlockStateV1.Variant add = new Variant(baseVar); // Create a duplicate variant object so modifying it doesn't modify baseVar. + for (Entry part : parts.entrySet()) // Put all the parts with single variants for this permutation. + add.submodels.put(part.getKey(), Collections.singletonList(part.getValue())); + ret.add(add); + return ret; + } + + String name = sorted.get(depth); + List vars = map.get(sorted.get(depth)); + + if (vars != null) + { + for (ForgeBlockStateV1.Variant v : vars) + { + if (v != null) + { // We put this part variant in the permutation's map to add further in recursion, and then remove it afterward just in case. + parts.put(name, v); + getSubmodelPermutations(baseVar, sorted, map, depth + 1, parts, ret); + parts.remove(name); + } + } + } + else + { + getSubmodelPermutations(baseVar, sorted, map, depth + 1, parts, ret); + } + + return ret; + } + + private List getSubmodelPermutations(ForgeBlockStateV1.Variant baseVar, Map> variants) + { + List sorted = Lists.newArrayList(variants.keySet()); + Collections.sort(sorted); // Sort to get consistent results. + return getSubmodelPermutations(baseVar, sorted, variants, 0, new HashMap(), new ArrayList()); + } + } + + public static class Variant + { + public static final Object SET_VALUE = new Object(); + + private ResourceLocation model = null; + private boolean modelSet = false; + private Optional rotation = Optional.absent(); + private Optional uvLock = Optional.absent(); + private Optional weight = Optional.absent(); + private Map textures = Maps.newHashMap(); + private Map> submodels = Maps.newHashMap(); + private Map simpleSubmodels = Maps.newHashMap(); // Makeshift Set to allow us to "remove" (replace value with null) singleParts when needed. + private Map customData = Maps.newHashMap(); + + private Variant(){} + /** + * Clone a variant. + * @param other Variant to clone. + */ + private Variant(ForgeBlockStateV1.Variant other) + { + this.model = other.model; + this.modelSet = other.modelSet; + this.rotation = other.rotation; + this.uvLock = other.uvLock; + this.weight = other.weight; + this.textures.putAll(other.textures); + this.submodels.putAll(other.submodels); + this.simpleSubmodels.putAll(other.simpleSubmodels); + this.customData.putAll(other.customData); + } + + /** + * Sets values in this variant to the input's values only if they haven't been set already. Essentially inherits values from o. + */ + ForgeBlockStateV1.Variant sync(ForgeBlockStateV1.Variant parent) + { + if (!this.modelSet) this.model = parent.model; + if (!this.rotation.isPresent()) this.rotation = parent.rotation; + if (!this.uvLock.isPresent()) this.uvLock = parent.uvLock; + if (!this.weight.isPresent()) this.weight = parent.weight; + + for (Entry e : parent.textures.entrySet()) + { + if (!this.textures.containsKey(e.getKey())) + this.textures.put(e.getKey(), e.getValue()); + } + + mergeModelPartVariants(this.submodels, parent.submodels); + + for (Entry e : parent.simpleSubmodels.entrySet()) + { + if (!this.simpleSubmodels.containsKey(e.getKey())) + this.simpleSubmodels.put(e.getKey(), e.getValue()); + } + + for (Entry e : parent.customData.entrySet()) + { + if (!this.customData.containsKey(e.getKey())) + this.customData.put(e.getKey(), e.getValue()); + } + + return this; + } + + /** + * Inherits model parts from a parent, creating deep clones of all Variants. + */ + Map> mergeModelPartVariants(Map> output, Map> input) + { + for (Entry> e : input.entrySet()) + { + String key = e.getKey(); + if (!output.containsKey(key)) + { + List variants = e.getValue(); + + if (variants != null) + { + List newVariants = Lists.newArrayListWithCapacity(variants.size()); + + for (ForgeBlockStateV1.Variant v : variants) + newVariants.add(new Variant(v)); + + output.put(key, newVariants); + } + else + output.put(key, variants); + } + } + return output; + } + + protected SubModel asGenericSubModel() + { + return new SubModel(rotation.or(ModelRotation.X0_Y0), uvLock.or(false), getTextures(), model, getCustomData()); + } + + /** + * Gets a list containing the single variant of each part. + * Will throw an error if this Variant has multiple variants for a submodel. + */ + public ImmutableMap getOnlyPartsVariant() + { + if (submodels.size() > 0) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + + for (Entry> entry : submodels.entrySet()) + { + List part = entry.getValue(); + + if (part != null) + { + if (part.size() == 1) + builder.put(entry.getKey(), part.get(0).asGenericSubModel()); + else + throw new RuntimeException("Something attempted to get the list of submodels " + + "for a variant with model \"" + model + "\", when this variant " + + "contains multiple variants for submodel " + entry.getKey()); + } + } + return builder.build(); + } + else { + return ImmutableMap.of(); + } + } + + public static class Deserializer implements JsonDeserializer + { + static Variant.Deserializer INSTANCE = new Deserializer(); + + /** Used once (then set null) for the key to put a simple submodel declaration under in the submodel map. */ + public String simpleSubmodelKey = null; + + protected ResourceLocation getBlockLocation(String location) + { + ResourceLocation tmp = new ResourceLocation(location); + return new ResourceLocation(tmp.getResourceDomain(), "block/" + tmp.getResourcePath()); + } + + /** Throws an error if there are submodels in this submodel. */ + private void throwIfNestedSubmodels(ForgeBlockStateV1.Variant submodel) + { + if (submodel.submodels.size() > 0) + throw new UnsupportedOperationException("Forge BlockStateLoader V1 does not support nested submodels."); + } + + @Override + public ForgeBlockStateV1.Variant deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException + { + ForgeBlockStateV1.Variant ret = new Variant(); + JsonObject json = element.getAsJsonObject(); + + if (json.has("model")) + { // Load base model location. + if (json.get("model").isJsonNull()) + ret.model = null; // Allow overriding base model to remove it from a state. + else + ret.model = getBlockLocation(JsonUtils.getJsonObjectStringFieldValue(json, "model")); + ret.modelSet = true; + } + + if (json.has("textures")) + { // Load textures map. + for (Entry e : json.get("textures").getAsJsonObject().entrySet()) + { + if (e.getValue().isJsonNull()) + ret.textures.put(e.getKey(), null); + else + ret.textures.put(e.getKey(), e.getValue().getAsString()); + } + } + + if (json.has("x") || json.has("y")) + { // Load rotation values. + int x = JsonUtils.getJsonObjectIntegerFieldValueOrDefault(json, "x", 0); + int y = JsonUtils.getJsonObjectIntegerFieldValueOrDefault(json, "y", 0); + ret.rotation = Optional.of(ModelRotation.getModelRotation(x, y)); + if (ret.rotation == null) + throw new JsonParseException("Invalid BlockModelRotation x: " + x + " y: " + y); + } + + if (json.has("uvlock")) + { // Load uvlock. + ret.uvLock = Optional.of(JsonUtils.getJsonObjectBooleanFieldValue(json, "uvlock")); + } + + if (json.has("weight")) + { // Load weight. + ret.weight = Optional.of(JsonUtils.getJsonObjectIntegerFieldValue(json, "weight")); + } + + if (json.has("submodel")) + { // Load submodels. + JsonElement submodels = json.get("submodel"); + + if (submodels.isJsonPrimitive()) + { // Load a simple submodel declaration. + if (simpleSubmodelKey == null) + throw new RuntimeException("Attempted to use a simple submodel declaration outside a valid state variant declaration."); + String key = simpleSubmodelKey; + simpleSubmodelKey = null; + + ret.model = getBlockLocation(submodels.getAsString()); + ret.modelSet = true; + ForgeBlockStateV1.Variant dummyVar = new Variant(); // Create a dummy Variant to use as the owner of the simple submodel. + dummyVar.submodels.put(key, Collections.singletonList(ret)); + dummyVar.simpleSubmodels = Collections.singletonMap(key, Variant.SET_VALUE); + return dummyVar; + } + else + { // Load full submodel declarations. + // Clear the simple submodel key so that when deserializing submodels, the deserializer doesn't use the key. + simpleSubmodelKey = null; + + for (Entry submodel : submodels.getAsJsonObject().entrySet()) + { + JsonElement varEl = submodel.getValue(); + List submodelVariants; + + if (varEl.isJsonArray()) + { // Multiple variants of the submodel. + submodelVariants = Lists.newArrayList(); + for (JsonElement e : varEl.getAsJsonArray()) + submodelVariants.add((ForgeBlockStateV1.Variant)context.deserialize(e, ForgeBlockStateV1.Variant.class)); + } + else if (varEl.isJsonNull()) + { + submodelVariants = null; + } + else + { + submodelVariants = Collections.singletonList((ForgeBlockStateV1.Variant)context.deserialize(varEl, ForgeBlockStateV1.Variant.class)); + } + + if (submodelVariants != null) // Throw an error if there are submodels inside a submodel. + for (ForgeBlockStateV1.Variant part : submodelVariants) + throwIfNestedSubmodels(part); + + ret.submodels.put(submodel.getKey(), submodelVariants); + // Put null to remove this submodel from the list of simple submodels when something inherits this submodel. + ret.simpleSubmodels.put(submodel.getKey(), null); + } + } + } + + if (json.has("custom")) + { + for (Entry e : json.get("custom").getAsJsonObject().entrySet()) + { + if (e.getValue().isJsonNull()) + ret.customData.put(e.getKey(), null); + else + ret.customData.put(e.getKey(), e.getValue().toString()); + } + } + + simpleSubmodelKey = null; + + return ret; + } + } + + public ResourceLocation getModel() { return model; } + public boolean isModelSet() { return modelSet; } + public Optional getRotation() { return rotation; } + public Optional getUvLock() { return uvLock; } + public Optional getWeight() { return weight; } + public ImmutableMap getTextures() { return ImmutableMap.copyOf(textures); } + public ImmutableMap> getSubmodels() { return ImmutableMap.copyOf(submodels); } + public ImmutableMap getCustomData() { return ImmutableMap.copyOf(customData); } + } +} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/client/model/IModelCustomData.java b/src/main/java/net/minecraftforge/client/model/IModelCustomData.java new file mode 100644 index 000000000..da0468699 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/IModelCustomData.java @@ -0,0 +1,12 @@ +package net.minecraftforge.client.model; + +import com.google.common.collect.ImmutableMap; + +public interface IModelCustomData extends IModel +{ + /** + * Allows the model to process custom data from the variant definition + * @return a new model, with data applied + */ + IModel process(ImmutableMap customData); +} diff --git a/src/main/java/net/minecraftforge/client/model/IRetexturableModel.java b/src/main/java/net/minecraftforge/client/model/IRetexturableModel.java new file mode 100644 index 000000000..0e6139dd6 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/IRetexturableModel.java @@ -0,0 +1,24 @@ +package net.minecraftforge.client.model; + +import com.google.common.collect.ImmutableMap; + +public interface IRetexturableModel extends IModel +{ + /** + * Applies new textures to the model. + * The returned model should be independent of the accessed one, + * as a model should be able to be retextured multiple times producing + * a separate model each time. + * + * The input map MAY map to NULL which should be used to indicate the + * texture was removed. Handling of that is up to the model itself. + * Such as using default, missing texture, or removing vertices. + * + * The input should be considered a DIFF of the old textures, not a + * replacement as it may not contain everything. + * + * @param textures New + * @return Model with textures applied. + */ + IModel retexture(ImmutableMap textures); +} diff --git a/src/main/java/net/minecraftforge/client/model/ISmartVariant.java b/src/main/java/net/minecraftforge/client/model/ISmartVariant.java new file mode 100644 index 000000000..58d1711f0 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/ISmartVariant.java @@ -0,0 +1,8 @@ +package net.minecraftforge.client.model; + +import net.minecraft.client.renderer.block.model.ModelBlock; + +public interface ISmartVariant +{ + IModel process(IModel base, ModelLoader loader); +} diff --git a/src/main/java/net/minecraftforge/client/model/ModelLoader.java b/src/main/java/net/minecraftforge/client/model/ModelLoader.java index b770e31a6..3701a9f0e 100644 --- a/src/main/java/net/minecraftforge/client/model/ModelLoader.java +++ b/src/main/java/net/minecraftforge/client/model/ModelLoader.java @@ -10,6 +10,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -21,6 +22,8 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.BlockModelShapes; import net.minecraft.client.renderer.ItemMeshDefinition; import net.minecraft.client.renderer.ItemModelMesher; +import net.minecraft.client.renderer.block.model.BlockPart; +import net.minecraft.client.renderer.block.model.BlockPartFace; import net.minecraft.client.renderer.block.model.ItemCameraTransforms; import net.minecraft.client.renderer.block.model.ItemModelGenerator; import net.minecraft.client.renderer.block.model.ModelBlock; @@ -40,6 +43,7 @@ import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.client.resources.model.ModelRotation; import net.minecraft.client.resources.model.WeightedBakedModel; import net.minecraft.item.Item; +import net.minecraft.util.EnumFacing; import net.minecraft.util.IRegistry; import net.minecraft.util.ResourceLocation; import net.minecraftforge.client.event.TextureStitchEvent; @@ -190,7 +194,7 @@ public class ModelLoader extends ModelBakery loadingModels.remove(location); } - private class VanillaModelWrapper implements IModel + private class VanillaModelWrapper implements IRetexturableModel { private final ResourceLocation location; private final ModelBlock model; @@ -273,6 +277,67 @@ public class ModelLoader extends ModelBakery { return ModelRotation.X0_Y0; } + + @Override + public IModel retexture(ImmutableMap textures) + { + if (textures.isEmpty()) + return this; + + List elements = Lists.newArrayList(); //We have to duplicate this so we can edit it below. + for (BlockPart part : (List)this.model.getElements()) + { + elements.add(new BlockPart(part.positionFrom, part.positionTo, Maps.newHashMap(part.mapFaces), part.partRotation, part.shade)); + } + + ModelBlock neweModel = new ModelBlock(this.model.getParentLocation(), elements, + Maps.newHashMap(this.model.textures), this.model.isAmbientOcclusion(), this.model.isGui3d(), //New Textures man VERY IMPORTANT + new ItemCameraTransforms(this.model.getThirdPersonTransform(), this.model.getFirstPersonTransform(), this.model.getHeadTransform(), this.model.getInGuiTransform())); + neweModel.name = this.model.name; + neweModel.parent = this.model.parent; + + Set removed = Sets.newHashSet(); + + for (Entry e : textures.entrySet()) + { + if (e.getValue() == null) + { + removed.add(e.getKey()); + neweModel.textures.remove(e.getKey()); + } + else + neweModel.textures.put(e.getKey(), e.getValue()); + } + + // Map the model's texture references as if it was the parent of a model with the retexture map as its textures. + Map remapped = Maps.newHashMap(); + + for (Entry e : (Set>)neweModel.textures.entrySet()) + { + if (e.getValue().startsWith("#")) + { + String key = e.getValue().substring(1); + if (neweModel.textures.containsKey(key)) + remapped.put(e.getKey(), (String)neweModel.textures.get(key)); + } + } + + neweModel.textures.putAll(remapped); + + //Remove any faces that use a null texture, this is for performance reasons, also allows some cool layering stuff. + for (BlockPart part : (List)neweModel.getElements()) + { + Iterator> itr = part.mapFaces.entrySet().iterator(); + while (itr.hasNext()) + { + Entry entry = itr.next(); + if (removed.contains(entry.getValue().texture)) + itr.remove(); + } + } + + return new VanillaModelWrapper(location, neweModel); + } } public static class UVLock implements IModelState @@ -336,7 +401,15 @@ public class ModelLoader extends ModelBakery { ResourceLocation loc = v.getModelLocation(); locations.add(loc); - IModel model = new WeightedPartWrapper(getModel(loc)); + + IModel model = getModel(loc); + if (v instanceof ISmartVariant) + { + model = ((ISmartVariant)v).process(model, ModelLoader.this); + textures.addAll(model.getTextures()); // Kick this, just in case. + } + + model = new WeightedPartWrapper(model); models.add(model); builder.put(model, new TRSRTransformation(v.getRotation())); } @@ -449,7 +522,7 @@ public class ModelLoader extends ModelBakery } catch(IOException e) { - if(loader.isLoading) + if(loader.isLoading) //ToDo: Make this less gaging, hides missing models.. { // holding error until onPostBakeEvent } diff --git a/src/main/java/net/minecraftforge/client/model/MultiModel.java b/src/main/java/net/minecraftforge/client/model/MultiModel.java new file mode 100644 index 000000000..c11572652 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/MultiModel.java @@ -0,0 +1,220 @@ +package net.minecraftforge.client.model; + +import java.util.Collection; +import java.util.EnumMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.commons.lang3.tuple.Pair; + +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.block.model.ItemCameraTransforms; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.renderer.vertex.VertexFormat; +import net.minecraft.client.resources.model.ModelRotation; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.ResourceLocation; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +public class MultiModel implements IModel +{ + public static class Baked implements IFlexibleBakedModel + { + protected final IFlexibleBakedModel base; + protected final ImmutableMap parts; + + protected final IFlexibleBakedModel internalBase; + protected final ImmutableList general; + protected final ImmutableMap> faces; + + public Baked(IFlexibleBakedModel base, ImmutableMap parts) + { + this.base = base; + this.parts = parts; + + if (base != null) + internalBase = base; + else + { + Iterator iter = parts.values().iterator(); + if (iter.hasNext()) + internalBase = iter.next(); + else + throw new RuntimeException("No base model or submodel provided for this MultiModel.Baked."); + } + + // Create map of each face's quads. + EnumMap> faces = Maps.newEnumMap(EnumFacing.class); + + for (EnumFacing face : EnumFacing.values()) + { + ImmutableList.Builder faceQuads = ImmutableList.builder(); + if (base != null) + faceQuads.addAll(base.getFaceQuads(face)); + for (IFlexibleBakedModel bakedPart : parts.values()) + faceQuads.addAll(bakedPart.getFaceQuads(face)); + faces.put(face, faceQuads.build()); + } + + this.faces = Maps.immutableEnumMap(faces); + + // Create list of general quads. + ImmutableList.Builder genQuads = ImmutableList.builder(); + if (base != null) + genQuads.addAll(base.getGeneralQuads()); + for (IFlexibleBakedModel bakedPart : parts.values()) + genQuads.addAll(bakedPart.getGeneralQuads()); + general = genQuads.build(); + } + + @Override + public boolean isAmbientOcclusion() + { + return internalBase.isAmbientOcclusion(); + } + + @Override + public boolean isGui3d() + { + return internalBase.isGui3d(); + } + + @Override + public boolean isBuiltInRenderer() + { + return internalBase.isBuiltInRenderer(); + } + + @Override + public TextureAtlasSprite getTexture() + { + return internalBase.getTexture(); + } + + @Override + public ItemCameraTransforms getItemCameraTransforms() + { + return internalBase.getItemCameraTransforms(); + } + + @Override + public List getFaceQuads(EnumFacing side) + { + return faces.get(side); + } + + @Override + public List getGeneralQuads() + { + return general; + } + + @Override + public VertexFormat getFormat() + { + return internalBase.getFormat(); + } + + public IFlexibleBakedModel getBaseModel() + { + return base; + } + + public Map getParts() + { + return parts; + } + } + + protected final IModel base; + protected final IModelState baseState; + protected final Map> parts; + + public MultiModel(IModel base, IModelState baseState, ImmutableMap> parts) + { + this.base = base; + this.baseState = baseState; + this.parts = parts; + } + + public MultiModel(IModel base, IModelState baseState, Map> parts) + { + this(base, baseState, ImmutableMap.copyOf(parts)); + } + + @Override + public Collection getDependencies() + { + Set deps = Sets.newHashSet(); + + if (base != null) + deps.addAll(base.getDependencies()); + + for (Pair pair : parts.values()) + deps.addAll(pair.getLeft().getDependencies()); + + return deps; + } + + @Override + public Collection getTextures() + { + Set deps = Sets.newHashSet(); + + if (base != null) + deps.addAll(base.getTextures()); + + for (Pair pair : parts.values()) + deps.addAll(pair.getLeft().getTextures()); + + return deps; + } + + @Override + public IFlexibleBakedModel bake(IModelState state, VertexFormat format, Function bakedTextureGetter) + { + IFlexibleBakedModel bakedBase = null; + + if (base != null) + bakedBase = base.bake(baseState, format, bakedTextureGetter); + + ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); + + for (Entry> entry : parts.entrySet()) + { + Pair pair = entry.getValue(); + mapBuilder.put(entry.getKey(), pair.getLeft().bake(pair.getRight(), format, bakedTextureGetter)); + } + + return new Baked(bakedBase, mapBuilder.build()); + } + + @Override + public IModelState getDefaultState() + { + return ModelRotation.X0_Y0; + } + + /** + * @return The base model of this MultiModel. May be null. + */ + public IModel getBaseModel() + { + return base; + } + + /** + * @return A map of the submodel name to its IModel and IModelState. + */ + public Map> getParts() + { + return parts; + } +} diff --git a/src/main/resources/forge_at.cfg b/src/main/resources/forge_at.cfg index 8016e3a9f..26ce6a42c 100644 --- a/src/main/resources/forge_at.cfg +++ b/src/main/resources/forge_at.cfg @@ -154,3 +154,5 @@ public net.minecraft.util.EnumFacing field_82609_l # VALUES public net.minecraft.util.EnumFacing field_176754_o # HORIZONTALS public net.minecraft.client.renderer.WorldRenderer func_78909_a(I)I # getColorIndex public net.minecraft.client.renderer.WorldRenderer func_178972_a(IIIII)V # putColorRGBA +# ModelBlock Constructor +public net.minecraft.client.renderer.block.model.ModelBlock (Lnet/minecraft/util/ResourceLocation;Ljava/util/List;Ljava/util/Map;ZZLnet/minecraft/client/renderer/block/model/ItemCameraTransforms;)V diff --git a/src/test/java/net/minecraftforge/debug/ForgeBlockStatesLoaderDebug.java b/src/test/java/net/minecraftforge/debug/ForgeBlockStatesLoaderDebug.java new file mode 100644 index 000000000..137b17fc1 --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/ForgeBlockStatesLoaderDebug.java @@ -0,0 +1,86 @@ +package net.minecraftforge.debug; + +import java.util.Map; +import java.util.Map.Entry; + +import com.google.common.base.Function; +import com.google.common.collect.Maps; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockWall; +import net.minecraft.block.properties.IProperty; +import net.minecraft.block.state.BlockState; +import net.minecraft.block.state.IBlockState; +import net.minecraft.client.renderer.block.statemap.IStateMapper; +import net.minecraft.client.renderer.block.statemap.StateMap; +import net.minecraft.client.renderer.block.statemap.StateMapperBase; +import net.minecraft.client.resources.model.ModelBakery; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.init.Blocks; +import net.minecraft.item.Item; +import net.minecraft.item.ItemMultiTexture; +import net.minecraft.item.ItemStack; +import net.minecraftforge.client.model.ModelLoader; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.Mod.EventHandler; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.registry.GameData; +import net.minecraftforge.fml.common.registry.GameRegistry; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +@Mod(modid = ForgeBlockStatesLoaderDebug.MODID) +public class ForgeBlockStatesLoaderDebug { + public static final String MODID = "ForgeBlockStatesLoader"; + public static final String ASSETS = "forgeblockstatesloader:"; + + public static final String nameCustomWall = "custom_wall"; + public static final BlockWall blockCustomWall = new BlockWall(Blocks.cobblestone); + public static final ItemMultiTexture itemCustomWall = new ItemMultiTexture(blockCustomWall, blockCustomWall, new Function() + { + @Override + public String apply(ItemStack stack) + { + return BlockWall.EnumType.byMetadata(stack.getMetadata()).getUnlocalizedName(); + } + }); + + @EventHandler + public void preInit(FMLPreInitializationEvent event) + { + blockCustomWall.setUnlocalizedName(MODID + ".customWall"); + GameRegistry.registerBlock(blockCustomWall, null, nameCustomWall); + GameRegistry.registerItem(itemCustomWall, nameCustomWall); + GameData.getBlockItemMap().put(blockCustomWall, itemCustomWall); + + if (event.getSide() == Side.CLIENT) + preInitClient(event); + } + + @SideOnly(Side.CLIENT) + public void preInitClient(FMLPreInitializationEvent event) + { + ModelLoader.setCustomStateMapper(blockCustomWall, new IStateMapper() + { + StateMap stateMap = new StateMap.Builder().setProperty(BlockWall.VARIANT).setBuilderSuffix("_wall").build(); + @Override + public Map putStateModelLocations(Block block) + { + Map map = (Map) stateMap.putStateModelLocations(block); + Map newMap = Maps.newHashMap(); + + for (Entry e : map.entrySet()) + { + ModelResourceLocation loc = e.getValue(); + newMap.put(e.getKey(), new ModelResourceLocation(ASSETS + loc.getResourcePath(), loc.getVariant())); + } + + return newMap; + } + }); + Item customWallItem = Item.getItemFromBlock(blockCustomWall); + ModelLoader.setCustomModelResourceLocation(customWallItem, 0, new ModelResourceLocation(ASSETS + "cobblestone_wall", "inventory")); + ModelLoader.setCustomModelResourceLocation(customWallItem, 1, new ModelResourceLocation(ASSETS + "mossy_cobblestone_wall", "inventory")); + ModelBakery.addVariantName(customWallItem, ASSETS + "cobblestone_wall", ASSETS + "mossy_cobblestone_wall"); + } +} diff --git a/src/test/resources/assets/forgeblockstatesloader/blockstates/cobblestone_wall.json b/src/test/resources/assets/forgeblockstatesloader/blockstates/cobblestone_wall.json new file mode 100644 index 000000000..9ee3db701 --- /dev/null +++ b/src/test/resources/assets/forgeblockstatesloader/blockstates/cobblestone_wall.json @@ -0,0 +1,29 @@ +{ + "forge_marker": 1, + "defaults": { + "textures": {"wall": "blocks/cobblestone"}, + "model": "cobblestone_wall_post", + "uvlock": true // This and all other properties of "defaults" will be inherited by simple submodels. They will NOT be inherited by named submodels. + }, + "variants": { + "north": { + "true": {"submodel": "forgeblockstatesloader:wall_connect"}, // Simple submodel declaration. You can also specify multiple submodels for a variant. + "false": {} + }, + "south": { + "true": {"submodel": "forgeblockstatesloader:wall_connect", "y": 180}, + "false": {} + }, + "east": { + "true": {"submodel": "forgeblockstatesloader:wall_connect", "y": 90}, // Submodel will be rotated. + "false": {} + }, + "west": { + "true": {"submodel": "forgeblockstatesloader:wall_connect", "y": 270}, + "false": {} + }, + "up": {"true": {}, "false": {}}, // Must have this in here or the blockstates loader will not know of all the properties and values, and it will create the wrong vanilla state strings. + "east=false,north=true,south=true,up=false,west=false": {"model": null}, // Fully specified variant, will inherit from variants above, but remove the model set in "defaults", removing the wall post. + "east=true,north=false,south=false,up=false,west=true": {"model": null} + } +} diff --git a/src/test/resources/assets/forgeblockstatesloader/blockstates/mossy_cobblestone_wall.json b/src/test/resources/assets/forgeblockstatesloader/blockstates/mossy_cobblestone_wall.json new file mode 100644 index 000000000..853c9a342 --- /dev/null +++ b/src/test/resources/assets/forgeblockstatesloader/blockstates/mossy_cobblestone_wall.json @@ -0,0 +1,29 @@ +{ + "forge_marker": 1, + "defaults": { + "textures": {"wall": "blocks/cobblestone_mossy"}, + "model": "cobblestone_wall_post", + "uvlock": true + }, + "variants": { + "north": { + "true": {"submodel": "forgeblockstatesloader:wall_connect"}, + "false": {} + }, + "south": { + "true": {"submodel": "forgeblockstatesloader:wall_connect", "y": 180}, + "false": {} + }, + "east": { + "true": {"submodel": "forgeblockstatesloader:wall_connect", "y": 90}, + "false": {} + }, + "west": { + "true": {"submodel": "forgeblockstatesloader:wall_connect", "y": 270}, + "false": {} + }, + "up": {"true": {}, "false": {}}, + "east=false,north=true,south=true,up=false,west=false": {"model": null}, + "east=true,north=false,south=false,up=false,west=true": {"model": null} + } +} diff --git a/src/test/resources/assets/forgeblockstatesloader/models/block/wall_connect.json b/src/test/resources/assets/forgeblockstatesloader/models/block/wall_connect.json new file mode 100644 index 000000000..57e267e51 --- /dev/null +++ b/src/test/resources/assets/forgeblockstatesloader/models/block/wall_connect.json @@ -0,0 +1,18 @@ +{ + "textures": { + "particle": "#wall" + }, + "elements": [ + { "from": [ 5, 0, 0 ], + "to": [ 11, 13, 8 ], + "faces": { + "down": { "uv": [ 5, 0, 11, 8 ], "texture": "#wall", "cullface": "down" }, + "up": { "uv": [ 5, 0, 11, 8 ], "texture": "#wall" }, + "north": { "uv": [ 5, 3, 11, 16 ], "texture": "#wall", "cullface": "north" }, + "west": { "uv": [ 0, 3, 8, 16 ], "texture": "#wall" }, + "east": { "uv": [ 0, 3, 8, 16 ], "texture": "#wall" } + }, + "__comment": "North wall" + } + ] +} diff --git a/src/test/resources/assets/forgeblockstatesloader/models/item/cobblestone_wall.json b/src/test/resources/assets/forgeblockstatesloader/models/item/cobblestone_wall.json new file mode 100644 index 000000000..079cb13fe --- /dev/null +++ b/src/test/resources/assets/forgeblockstatesloader/models/item/cobblestone_wall.json @@ -0,0 +1,6 @@ +{ + "parent": "block/wall_inventory", + "textures": { + "wall": "blocks/cobblestone" + } +} diff --git a/src/test/resources/assets/forgeblockstatesloader/models/item/mossy_cobblestone_wall.json b/src/test/resources/assets/forgeblockstatesloader/models/item/mossy_cobblestone_wall.json new file mode 100644 index 000000000..f5f126f53 --- /dev/null +++ b/src/test/resources/assets/forgeblockstatesloader/models/item/mossy_cobblestone_wall.json @@ -0,0 +1,6 @@ +{ + "parent": "block/wall_inventory", + "textures": { + "wall": "blocks/cobblestone_mossy" + } +}