Hook BlockState's Json loading to add support for simplified Forge format.

See https://github.com/MinecraftForge/MinecraftForge/pull/1885 for more details.
This commit is contained in:
Lex Manos 2015-05-20 02:54:33 -07:00
parent d3ab6d36a4
commit 267e1ee62f
15 changed files with 1251 additions and 3 deletions

View file

@ -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_)

View file

@ -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<ModelBlockDefinition.Variants> variants = Lists.newArrayList();
for (Entry<String, Collection<ForgeBlockStateV1.Variant>> entry : v1.variants.asMap().entrySet())
{ // Convert Version1 variants into vanilla variants for the ModelBlockDefinition.
List<ModelBlockDefinition.Variant> 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<String, String> textures;
private final ResourceLocation model;
private final ImmutableMap<String, String> customData;
public SubModel(ModelRotation rotation, boolean uvLock, ImmutableMap<String, String> textures, ResourceLocation model, ImmutableMap<String, String> 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<String, String> getTextures() { return textures; }
public ResourceLocation getModelLocation() { return model; }
public ImmutableMap<String, String> getCustomData() { return customData; }
}
private static class ForgeVariant extends ModelBlockDefinition.Variant implements ISmartVariant
{
private final ImmutableMap<String, String> textures;
private final ImmutableMap<String, SubModel> parts;
private final ImmutableMap<String, String> customData;
public ForgeVariant(ResourceLocation model, ModelRotation rotation, boolean uvLock, int weight, ImmutableMap<String, String> textures, ImmutableMap<String, SubModel> parts, ImmutableMap<String, String> 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<String, String> textureMap, ImmutableMap<String, String> 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<String, Pair<IModel, IModelState>> models = ImmutableMap.builder();
for (Entry<String, SubModel> 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<String, String> e: this.textures.entrySet())
buf.append(" ").append(e.getKey()).append(" = ").append(e.getValue());
return buf.toString();
}
}
}

View file

@ -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<String, ForgeBlockStateV1.Variant> variants = HashMultimap.create();
public static class Deserializer implements JsonDeserializer<ForgeBlockStateV1>
{
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<String, Map<String, ForgeBlockStateV1.Variant>> condensed = Maps.newHashMap(); // map(property name -> map(property value -> variant))
Multimap<String, ForgeBlockStateV1.Variant> specified = HashMultimap.create(); // Multimap containing all the states specified with "property=value".
for (Entry<String, JsonElement> 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<String, ForgeBlockStateV1.Variant> subs = Maps.newHashMap();
condensed.put(e.getKey(), subs);
for (Entry<String, JsonElement> 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<String, ForgeBlockStateV1.Variant> permutations = getPermutations(condensed); // Get permutations of Forge-style states.
for (Entry<String, Collection<ForgeBlockStateV1.Variant>> e : specified.asMap().entrySet())
{ // Make fully-specified variants override Forge variant permutations, inheriting the permutations' values.
Collection<ForgeBlockStateV1.Variant> baseVars = permutations.get(e.getKey());
List<ForgeBlockStateV1.Variant> 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<String, ForgeBlockStateV1.Variant> 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<String, Object> 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<ForgeBlockStateV1.Variant> 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<ModelRotation> rotation = part.rotation;
part.sync(v);
part.simpleSubmodels.clear();
part.rotation = rotation;
}
}
if (v.textures != null)
{
for (Entry<String, String> 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<String, String> 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<ForgeBlockStateV1.Variant> part : v.submodels.values()) // Sync base variant's textures (including defaults) into all submodels.
{
for (ForgeBlockStateV1.Variant partVar : part)
{
for (Entry<String, String> 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<String, ForgeBlockStateV1.Variant> getPermutations(List<String> sorted, Map<String, Map<String, ForgeBlockStateV1.Variant>> base, int depth, String prefix, Multimap<String, ForgeBlockStateV1.Variant> ret, ForgeBlockStateV1.Variant parent)
{
if (depth == sorted.size())
{
ret.put(prefix, parent);
return ret;
}
String name = sorted.get(depth);
for (Entry<String, ForgeBlockStateV1.Variant> 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<String, ForgeBlockStateV1.Variant> getPermutations(Map<String, Map<String, ForgeBlockStateV1.Variant>> base)
{
List<String> sorted = Lists.newArrayList(base.keySet());
Collections.sort(sorted); // Sort to get consistent results.
return getPermutations(sorted, base, 0, "", HashMultimap.<String, ForgeBlockStateV1.Variant>create(), null);
}
private List<ForgeBlockStateV1.Variant> getSubmodelPermutations(ForgeBlockStateV1.Variant baseVar, List<String> sorted, Map<String, List<ForgeBlockStateV1.Variant>> map, int depth, Map<String, ForgeBlockStateV1.Variant> parts, List<ForgeBlockStateV1.Variant> 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<String, ForgeBlockStateV1.Variant> 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<ForgeBlockStateV1.Variant> 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<ForgeBlockStateV1.Variant> getSubmodelPermutations(ForgeBlockStateV1.Variant baseVar, Map<String, List<ForgeBlockStateV1.Variant>> variants)
{
List<String> sorted = Lists.newArrayList(variants.keySet());
Collections.sort(sorted); // Sort to get consistent results.
return getSubmodelPermutations(baseVar, sorted, variants, 0, new HashMap<String, ForgeBlockStateV1.Variant>(), new ArrayList<ForgeBlockStateV1.Variant>());
}
}
public static class Variant
{
public static final Object SET_VALUE = new Object();
private ResourceLocation model = null;
private boolean modelSet = false;
private Optional<ModelRotation> rotation = Optional.absent();
private Optional<Boolean> uvLock = Optional.absent();
private Optional<Integer> weight = Optional.absent();
private Map<String, String> textures = Maps.newHashMap();
private Map<String, List<ForgeBlockStateV1.Variant>> submodels = Maps.newHashMap();
private Map<String, Object> simpleSubmodels = Maps.newHashMap(); // Makeshift Set to allow us to "remove" (replace value with null) singleParts when needed.
private Map<String, String> 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<String, String> e : parent.textures.entrySet())
{
if (!this.textures.containsKey(e.getKey()))
this.textures.put(e.getKey(), e.getValue());
}
mergeModelPartVariants(this.submodels, parent.submodels);
for (Entry<String, Object> e : parent.simpleSubmodels.entrySet())
{
if (!this.simpleSubmodels.containsKey(e.getKey()))
this.simpleSubmodels.put(e.getKey(), e.getValue());
}
for (Entry<String, String> 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<String, List<ForgeBlockStateV1.Variant>> mergeModelPartVariants(Map<String, List<ForgeBlockStateV1.Variant>> output, Map<String, List<ForgeBlockStateV1.Variant>> input)
{
for (Entry<String, List<ForgeBlockStateV1.Variant>> e : input.entrySet())
{
String key = e.getKey();
if (!output.containsKey(key))
{
List<ForgeBlockStateV1.Variant> variants = e.getValue();
if (variants != null)
{
List<ForgeBlockStateV1.Variant> 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<String, SubModel> getOnlyPartsVariant()
{
if (submodels.size() > 0)
{
ImmutableMap.Builder<String, SubModel> builder = ImmutableMap.builder();
for (Entry<String, List<ForgeBlockStateV1.Variant>> entry : submodels.entrySet())
{
List<ForgeBlockStateV1.Variant> 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<ForgeBlockStateV1.Variant>
{
static Variant.Deserializer INSTANCE = new Deserializer();
/** Used <i>once</i> (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<String, JsonElement> 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<String, JsonElement> submodel : submodels.getAsJsonObject().entrySet())
{
JsonElement varEl = submodel.getValue();
List<ForgeBlockStateV1.Variant> 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<String, JsonElement> 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<ModelRotation> getRotation() { return rotation; }
public Optional<Boolean> getUvLock() { return uvLock; }
public Optional<Integer> getWeight() { return weight; }
public ImmutableMap<String, String> getTextures() { return ImmutableMap.copyOf(textures); }
public ImmutableMap<String, List<ForgeBlockStateV1.Variant>> getSubmodels() { return ImmutableMap.copyOf(submodels); }
public ImmutableMap<String, String> getCustomData() { return ImmutableMap.copyOf(customData); }
}
}

View file

@ -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<String, String> customData);
}

View file

@ -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<String, String> textures);
}

View file

@ -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);
}

View file

@ -10,6 +10,7 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; 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.BlockModelShapes;
import net.minecraft.client.renderer.ItemMeshDefinition; import net.minecraft.client.renderer.ItemMeshDefinition;
import net.minecraft.client.renderer.ItemModelMesher; 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.ItemCameraTransforms;
import net.minecraft.client.renderer.block.model.ItemModelGenerator; import net.minecraft.client.renderer.block.model.ItemModelGenerator;
import net.minecraft.client.renderer.block.model.ModelBlock; 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.ModelRotation;
import net.minecraft.client.resources.model.WeightedBakedModel; import net.minecraft.client.resources.model.WeightedBakedModel;
import net.minecraft.item.Item; import net.minecraft.item.Item;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.IRegistry; import net.minecraft.util.IRegistry;
import net.minecraft.util.ResourceLocation; import net.minecraft.util.ResourceLocation;
import net.minecraftforge.client.event.TextureStitchEvent; import net.minecraftforge.client.event.TextureStitchEvent;
@ -190,7 +194,7 @@ public class ModelLoader extends ModelBakery
loadingModels.remove(location); loadingModels.remove(location);
} }
private class VanillaModelWrapper implements IModel private class VanillaModelWrapper implements IRetexturableModel
{ {
private final ResourceLocation location; private final ResourceLocation location;
private final ModelBlock model; private final ModelBlock model;
@ -273,6 +277,67 @@ public class ModelLoader extends ModelBakery
{ {
return ModelRotation.X0_Y0; return ModelRotation.X0_Y0;
} }
@Override
public IModel retexture(ImmutableMap<String, String> textures)
{
if (textures.isEmpty())
return this;
List<BlockPart> elements = Lists.newArrayList(); //We have to duplicate this so we can edit it below.
for (BlockPart part : (List<BlockPart>)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<String> removed = Sets.newHashSet();
for (Entry<String, String> 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<String, String> remapped = Maps.newHashMap();
for (Entry<String, String> e : (Set<Entry<String, String>>)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<BlockPart>)neweModel.getElements())
{
Iterator<Entry<EnumFacing, BlockPartFace>> itr = part.mapFaces.entrySet().iterator();
while (itr.hasNext())
{
Entry<EnumFacing, BlockPartFace> entry = itr.next();
if (removed.contains(entry.getValue().texture))
itr.remove();
}
}
return new VanillaModelWrapper(location, neweModel);
}
} }
public static class UVLock implements IModelState public static class UVLock implements IModelState
@ -336,7 +401,15 @@ public class ModelLoader extends ModelBakery
{ {
ResourceLocation loc = v.getModelLocation(); ResourceLocation loc = v.getModelLocation();
locations.add(loc); 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); models.add(model);
builder.put(model, new TRSRTransformation(v.getRotation())); builder.put(model, new TRSRTransformation(v.getRotation()));
} }
@ -449,7 +522,7 @@ public class ModelLoader extends ModelBakery
} }
catch(IOException e) catch(IOException e)
{ {
if(loader.isLoading) if(loader.isLoading) //ToDo: Make this less gaging, hides missing models..
{ {
// holding error until onPostBakeEvent // holding error until onPostBakeEvent
} }

View file

@ -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<String, IFlexibleBakedModel> parts;
protected final IFlexibleBakedModel internalBase;
protected final ImmutableList<BakedQuad> general;
protected final ImmutableMap<EnumFacing, ImmutableList<BakedQuad>> faces;
public Baked(IFlexibleBakedModel base, ImmutableMap<String, IFlexibleBakedModel> parts)
{
this.base = base;
this.parts = parts;
if (base != null)
internalBase = base;
else
{
Iterator<IFlexibleBakedModel> 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<EnumFacing, ImmutableList<BakedQuad>> faces = Maps.newEnumMap(EnumFacing.class);
for (EnumFacing face : EnumFacing.values())
{
ImmutableList.Builder<BakedQuad> 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<BakedQuad> 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<BakedQuad> getFaceQuads(EnumFacing side)
{
return faces.get(side);
}
@Override
public List<BakedQuad> getGeneralQuads()
{
return general;
}
@Override
public VertexFormat getFormat()
{
return internalBase.getFormat();
}
public IFlexibleBakedModel getBaseModel()
{
return base;
}
public Map<String, IFlexibleBakedModel> getParts()
{
return parts;
}
}
protected final IModel base;
protected final IModelState baseState;
protected final Map<String, Pair<IModel, IModelState>> parts;
public MultiModel(IModel base, IModelState baseState, ImmutableMap<String, Pair<IModel, IModelState>> parts)
{
this.base = base;
this.baseState = baseState;
this.parts = parts;
}
public MultiModel(IModel base, IModelState baseState, Map<String, Pair<IModel, IModelState>> parts)
{
this(base, baseState, ImmutableMap.copyOf(parts));
}
@Override
public Collection<ResourceLocation> getDependencies()
{
Set<ResourceLocation> deps = Sets.newHashSet();
if (base != null)
deps.addAll(base.getDependencies());
for (Pair<IModel, IModelState> pair : parts.values())
deps.addAll(pair.getLeft().getDependencies());
return deps;
}
@Override
public Collection<ResourceLocation> getTextures()
{
Set<ResourceLocation> deps = Sets.newHashSet();
if (base != null)
deps.addAll(base.getTextures());
for (Pair<IModel, IModelState> pair : parts.values())
deps.addAll(pair.getLeft().getTextures());
return deps;
}
@Override
public IFlexibleBakedModel bake(IModelState state, VertexFormat format, Function<ResourceLocation, TextureAtlasSprite> bakedTextureGetter)
{
IFlexibleBakedModel bakedBase = null;
if (base != null)
bakedBase = base.bake(baseState, format, bakedTextureGetter);
ImmutableMap.Builder<String, IFlexibleBakedModel> mapBuilder = ImmutableMap.builder();
for (Entry<String, Pair<IModel, IModelState>> entry : parts.entrySet())
{
Pair<IModel, IModelState> 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<String, Pair<IModel, IModelState>> getParts()
{
return parts;
}
}

View file

@ -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.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_78909_a(I)I # getColorIndex
public net.minecraft.client.renderer.WorldRenderer func_178972_a(IIIII)V # putColorRGBA public net.minecraft.client.renderer.WorldRenderer func_178972_a(IIIII)V # putColorRGBA
# ModelBlock Constructor
public net.minecraft.client.renderer.block.model.ModelBlock <init>(Lnet/minecraft/util/ResourceLocation;Ljava/util/List;Ljava/util/Map;ZZLnet/minecraft/client/renderer/block/model/ItemCameraTransforms;)V

View file

@ -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<ItemStack, String>()
{
@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<IBlockState, ModelResourceLocation> map = (Map<IBlockState, ModelResourceLocation>) stateMap.putStateModelLocations(block);
Map<IBlockState, ModelResourceLocation> newMap = Maps.newHashMap();
for (Entry<IBlockState, ModelResourceLocation> 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");
}
}

View file

@ -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}
}
}

View file

@ -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}
}
}

View file

@ -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"
}
]
}

View file

@ -0,0 +1,6 @@
{
"parent": "block/wall_inventory",
"textures": {
"wall": "blocks/cobblestone"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "block/wall_inventory",
"textures": {
"wall": "blocks/cobblestone_mossy"
}
}