package net.minecraftforge.fml.common.registry; import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang3.Validate; import net.minecraft.block.Block; import net.minecraft.item.ItemBanner; import net.minecraft.item.ItemBlock; import net.minecraft.util.ObjectIntIdentityMap; import net.minecraft.util.RegistryNamespaced; import net.minecraft.util.RegistryNamespacedDefaultedByKey; import net.minecraft.util.ResourceLocation; import net.minecraftforge.fml.common.FMLLog; import net.minecraftforge.fml.common.functions.GenericIterableFactory; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterators; public class FMLControlledNamespacedRegistry<I> extends RegistryNamespacedDefaultedByKey { private final Class<I> superType; private Object optionalDefaultKey; private I optionalDefaultObject; private int maxId; private int minId; // aliases redirecting legacy names to the actual name, may need recursive application to find the final name. // these need to be registry specific, it's possible to only have a loosely linked item for a block which may get renamed by itself. private final Map<String, String> aliases = new HashMap<String, String>(); private BiMap<String, I> persistentSubstitutions; private BiMap<String, I> activeSubstitutions = HashBiMap.create(); FMLControlledNamespacedRegistry(Object defaultKey, int maxIdValue, int minIdValue, Class<I> type) { super(defaultKey); this.superType = type; this.optionalDefaultKey = defaultKey; this.maxId = maxIdValue; this.minId = minIdValue; } void validateContent(int maxId, String type, BitSet availabilityMap, Set<Integer> blockedIds, FMLControlledNamespacedRegistry<Block> iBlockRegistry) { for (I obj : typeSafeIterable()) { int id = getId(obj); Object name = getNameForObject(obj); // name lookup failed -> obj is not in the obj<->name map if (name == null) throw new IllegalStateException(String.format("Registry entry for %s %s, id %d, doesn't yield a name.", type, obj, id)); ResourceLocation loc = (name instanceof ResourceLocation) ? (ResourceLocation)name : null; String nameS = (name instanceof String) ? (String)name : (loc != null ? name.toString() : null); if (loc == null && nameS == null) throw new IllegalStateException(String.format("Registry entry for %s %s name is invalid, must be a String or ResourceLocation %s", type, obj, name)); // id lookup failed -> obj is not in the obj<->id map if (id < 0) throw new IllegalStateException(String.format("Registry entry for %s %s, name %s, doesn't yield an id.", type, obj, name)); // id is too high if (id > maxId) throw new IllegalStateException(String.format("Registry entry for %s %s, name %s uses the too large id %d.", type, obj, name)); // empty name if (name.toString().isEmpty()) throw new IllegalStateException(String.format("Registry entry for %s %s, id %d, yields an empty name.", type, obj, id)); // non-prefixed name if (name.toString().indexOf(':') == -1) throw new IllegalStateException(String.format("Registry entry for %s %s, id %d, has the non-prefixed name %s.", type, obj, id, name)); // id -> obj lookup is inconsistent if (getRaw(id) != obj) throw new IllegalStateException(String.format("Registry entry for id %d, name %s, doesn't yield the expected %s %s.", id, name, type, obj)); // name -> obj lookup is inconsistent if (!(activeSubstitutions.containsKey(name) || activeSubstitutions.containsValue(name)) && getRaw(nameS) != obj ) throw new IllegalStateException(String.format("Registry entry for name %s, id %d, doesn't yield the expected %s %s.", name, id, type, obj)); // name -> id lookup is inconsistent if (!(activeSubstitutions.containsKey(name) || activeSubstitutions.containsValue(name)) && getId(nameS) != id) throw new IllegalStateException(String.format("Registry entry for name %s doesn't yield the expected id %d.", name, id)); // id isn't marked as unavailable if (!availabilityMap.get(id)) throw new IllegalStateException(String.format("Registry entry for %s %s, id %d, name %s, marked as empty.", type, obj, id, name)); // entry is blocked, thus should be empty if (blockedIds.contains(id)) throw new IllegalStateException(String.format("Registry entry for %s %s, id %d, name %s, marked as dangling.", type, obj, id, name)); if (obj instanceof ItemBlock && !(obj instanceof ItemBanner)) //Dammet Mojang not linking Banners { Block block = ((ItemBlock) obj).block; // verify matching block entry if (iBlockRegistry.getId(block) != id) { throw new IllegalStateException(String.format("Registry entry for ItemBlock %s, id %d, is missing or uses the non-matching id %d.", obj, id, iBlockRegistry.getId(block))); } // verify id range if (id > GameData.MAX_BLOCK_ID) throw new IllegalStateException(String.format("ItemBlock %s uses the id %d outside the block id range", name, id)); } } } @SuppressWarnings("unchecked") void setFrom(FMLControlledNamespacedRegistry<?> registry) { set((FMLControlledNamespacedRegistry<I>) registry); } void set(FMLControlledNamespacedRegistry<I> registry) { if (this.superType != registry.superType) throw new IllegalArgumentException("incompatible registry"); this.optionalDefaultKey = registry.optionalDefaultKey; this.maxId = registry.maxId; this.minId = registry.minId; this.aliases.clear(); this.aliases.putAll(registry.aliases); this.activeSubstitutions.clear(); underlyingIntegerMap = new ObjectIntIdentityMap(); registryObjects.clear(); for (I thing : registry.typeSafeIterable()) { addObjectRaw(registry.getId(thing), registry.getNameForObject(thing), thing); } this.activeSubstitutions.putAll(registry.activeSubstitutions); } // public api /** * Add an object to the registry, trying to use the specified id. * * @deprecated register through {@link GameRegistry} instead. */ @Override @Deprecated public void register(int id, Object name, Object thing) { Validate.isInstanceOf(ResourceLocation.class, name); GameData.getMain().register(thing, name.toString(), id); } /** * DANGEROUS! EVIL! DO NOT USE! * * @deprecated register through {@link GameRegistry} instead. */ @Override @Deprecated public void putObject(Object objName, Object obj) { String name = objName.toString(); I thing = superType.cast(obj); if (name == null) throw new NullPointerException("Can't use a null-name for the registry."); if (name.isEmpty()) throw new IllegalArgumentException("Can't use an empty name for the registry."); if (thing == null) throw new NullPointerException("Can't add null-object to the registry."); name = new ResourceLocation(name).toString(); Object existingName = getNameForObject(thing); if (existingName == null) { FMLLog.bigWarning("Ignoring putObject(%s, %s), not resolvable", name, thing); } else if (existingName.equals(name)) { FMLLog.bigWarning("Ignoring putObject(%s, %s), already added", name, thing); } else { FMLLog.bigWarning("Ignoring putObject(%s, %s), adding alias to %s instead", name, thing, existingName); addAlias(name, existingName.toString()); } } /** * Fetch the object identified by the specified name or the default object. * * For blocks the default object is the air block, for items it's null. * * @param name Unique name identifying the object. * @return Registered object of the default object if it wasn't found- */ @Override public I getObject(Object name) { I object = null; if (name instanceof ResourceLocation) object = getRaw((ResourceLocation)name); if (name instanceof String) object = getRaw((String)name); return object == null ? this.optionalDefaultObject : object; } /** * Fetch the object identified by the specified id or the default object. * * For blocks the default object is the air block, for items it's null. * * @param id ID identifying the object. * @return Registered object of the default object if it wasn't found- */ @Override public I getObjectById(int id) { I object = getRaw(id); return object == null ? this.optionalDefaultObject : object; } /** * @deprecated use getObjectById instead */ @Deprecated public I get(int id) { return getObjectById(id); } /** * @deprecated use getObject instead */ @Deprecated public I get(String name) { return getObject(name); } /** * Get the id for the specified object. * * Don't hold onto the id across the world, it's being dynamically re-mapped as needed. * * Usually the name should be used instead of the id, if using the Block/Item object itself is * not suitable for the task. * * @param thing Block/Item object. * @return Block/Item id or -1 if it wasn't found. */ public int getId(I thing) { return getIDForObject(thing); } /** * Get the object identified by the specified id. * * @param id Block/Item id. * @return Block/Item object or null if it wasn't found. */ public I getRaw(int id) { return cast(super.getObjectById(id)); } /** * superType.cast appears to be expensive. Skip it for speed? * @param obj * @return */ @SuppressWarnings("unchecked") private I cast(Object obj) { return (I)(obj); } /** * Get the object identified by the specified name. * * @param name Block/Item name. * @return Block/Item object or null if it wasn't found. */ public I getRaw(String name) { return getRaw(new ResourceLocation(name)); } /** * Get the object identified by the specified name. * * @param name Block/Item name. * @return Block/Item object or null if it wasn't found. */ private I getRaw(ResourceLocation loc) { I ret = superType.cast(super.getObject(loc)); if (ret == null) // no match, try aliases recursively { String name = aliases.get(loc.toString()); if (name != null) return getRaw(name); } return ret; } /** * Determine if the registry has an entry for the specified name. * * Aliased names will be resolved as well. * * @param name Object name to check. * @return true if a matching entry was found. */ @Override public boolean containsKey(Object name) { boolean ret = super.containsKey(name); if (!ret) // no match, try aliases recursively { name = aliases.get(name); if (name != null) return containsKey(name); } return ret; } /** * Get the id for the specified object. * * Don't hold onto the id across the world, it's being dynamically re-mapped as needed. * * Usually the name should be used instead of the id, if using the Block/Item object itself is * not suitable for the task. * * @param itemName Block/Item registry name. * @return Block/Item id or -1 if it wasn't found. */ public int getId(String itemName) { I obj = getRaw(itemName); if (obj == null) return -1; return getId(obj); } /** * @deprecated use containsKey instead */ @Deprecated public boolean contains(String itemName) { return containsKey(itemName); } /* * This iterator is used by FML to visit the actual block sets, it should use the super.iterator method instead * Compare #iterator() */ public Iterable<I> typeSafeIterable() { return GenericIterableFactory.newCastingIterable(super.iterator(), superType); } // internal public void serializeInto(Map<String, Integer> idMapping) // for saving { for (I thing : this.typeSafeIterable()) { idMapping.put(getNameForObject(thing).toString(), getId(thing)); } } public void serializeAliases(Map<String, String> map) { map.putAll(this.aliases); } public void serializeSubstitutions(Set<String> set) { set.addAll(activeSubstitutions.keySet()); } private BitSet internalAvailabilityMap = new BitSet(); int add(int id, String name, Object thing) { return add(id, name, superType.cast(thing), internalAvailabilityMap); } /** * Add the specified object to the registry. * * @param id ID to use if available, auto-assigned otherwise. * @param name Name to use, prefixed by the mod id. * @param thing Object to add. * @param availabilityMap Map marking available IDs for auto assignment. * @return ID eventually allocated. */ int add(int id, String name, I thing, BitSet availabilityMap) { if (name == null) throw new NullPointerException(String.format("Can't use a null-name for the registry, object %s.", thing)); if (name.isEmpty()) throw new IllegalArgumentException(String.format("Can't use an empty name for the registry, object %s.", thing)); if (name.indexOf(':') == -1) throw new IllegalArgumentException(String.format("Can't add the name (%s) without a prefix, object %s", name, thing)); if (thing == null) throw new NullPointerException(String.format("Can't add null-object to the registry, name %s.", name)); if (optionalDefaultKey != null && optionalDefaultKey.toString().equals(name) && this.optionalDefaultObject == null) { this.optionalDefaultObject = thing; } if (getPersistentSubstitutions().containsValue(thing)) { throw new IllegalArgumentException(String.format("The object %s (%s) cannot be added to the registry. It is already being used as a substitute for %s", thing.getClass(), name, getPersistentSubstitutions().inverse().get(thing))); } int idToUse = id; if (idToUse < 0 || availabilityMap.get(idToUse)) { idToUse = availabilityMap.nextClearBit(minId); } if (idToUse > maxId) { throw new RuntimeException(String.format("Invalid id %d - maximum id range exceeded.", idToUse)); } if (getRaw(name) == thing) // already registered, return prev registration's id { FMLLog.bigWarning("The object %s has been registered twice for the same name %s.", thing, name); return getId(thing); } if (getRaw(name) != null) // duplicate name { throw new IllegalArgumentException(String.format("The name %s has been registered twice, for %s and %s.", name, getRaw(name), thing)); } if (getId(thing) >= 0) // duplicate object - but only if it's not being substituted { int foundId = getId(thing); Object otherThing = getRaw(foundId); throw new IllegalArgumentException(String.format("The object %s{%x} has been registered twice, using the names %s and %s. (Other object at this id is %s{%x})", thing, System.identityHashCode(thing), getNameForObject(thing), name, otherThing, System.identityHashCode(otherThing))); } if (GameData.isFrozen(this)) { FMLLog.bigWarning("The object %s (name %s) is being added too late.", thing, name); } if (activeSubstitutions.containsKey(name)) { thing = activeSubstitutions.get(name); } addObjectRaw(idToUse, new ResourceLocation(name), thing); FMLLog.finer("Registry add: %s %d %s (req. id %d)", name, idToUse, thing, id); return idToUse; } void addAlias(String from, String to) { aliases.put(from, to); FMLLog.finer("Registry alias: %s -> %s", from, to); } Map<String,Integer> getEntriesNotIn(FMLControlledNamespacedRegistry<I> registry) { Map<String,Integer> ret = new HashMap<String, Integer>(); for (I thing : this.typeSafeIterable()) { if (!registry.field_148758_b.containsKey(thing)) { if (!registry.activeSubstitutions.containsKey(getNameForObject(thing).toString())) { ret.put(getNameForObject(thing).toString(), getId(thing)); } } } return ret; } void dump() { List<Integer> ids = new ArrayList<Integer>(); for (I thing : this.typeSafeIterable()) { ids.add(getId(thing)); } // sort by id Collections.sort(ids); for (int id : ids) { I thing = getRaw(id); FMLLog.finer("Registry: %s %d %s", getNameForObject(thing), id, thing); } } /** * Version of addObject not using the API restricting overrides. */ private void addObjectRaw(int id, Object name, I thing) { if (name == null) throw new NullPointerException("The name to be added to the registry is null. This can only happen with a corrupted registry state. Reflection/ASM hackery? Registry bug?"); if (thing == null) throw new NullPointerException("The object to be added to the registry is null. This can only happen with a corrupted registry state. Reflection/ASM hackery? Registry bug?"); if (!superType.isInstance(thing)) throw new IllegalArgumentException("The object to be added to the registry is not of the right type. Reflection/ASM hackery? Registry bug?"); underlyingIntegerMap.put(thing, id); // obj <-> id super.putObject(name, thing); // name <-> obj } public I getDefaultValue() { return optionalDefaultObject; } public RegistryDelegate<I> getDelegate(I thing, Class<I> clazz) { return GameData.buildDelegate(thing, clazz); } void activateSubstitution(String nameToReplace) { if (getPersistentSubstitutions().containsKey(nameToReplace)) { activeSubstitutions.put(nameToReplace, getPersistentSubstitutions().get(nameToReplace)); } } void addSubstitutionAlias(String modId, String nameToReplace, Object toReplace) throws ExistingSubstitutionException { if (getPersistentSubstitutions().containsKey(nameToReplace) || getPersistentSubstitutions().containsValue(toReplace)) { FMLLog.severe("The substitution of %s has already occured. You cannot duplicate substitutions", nameToReplace); throw new ExistingSubstitutionException(nameToReplace, toReplace); } I replacement = cast(toReplace); I original = getRaw(nameToReplace); if (original == null) { throw new NullPointerException("The replacement target is not present. This won't work"); } if (!original.getClass().isAssignableFrom(replacement.getClass())) { FMLLog.severe("The substitute %s for %s (type %s) is type incompatible. This won't work", replacement.getClass().getName(), nameToReplace, original.getClass().getName()); throw new IncompatibleSubstitutionException(nameToReplace, replacement, original); } int existingId = getId(replacement); if (existingId != -1) { FMLLog.severe("The substitute %s for %s is registered into the game independently. This won't work", replacement.getClass().getName(), nameToReplace); throw new IllegalArgumentException("The object substitution is already registered. This won't work"); } getPersistentSubstitutions().put(nameToReplace, replacement); } private BiMap<String, I> getPersistentSubstitutions() { if (persistentSubstitutions == null) { persistentSubstitutions = GameData.getMain().getPersistentSubstitutionMap(superType); } return persistentSubstitutions; } @Override public void validateKey() { if (this.optionalDefaultKey != null) Validate.notNull(this.optionalDefaultObject); } FMLControlledNamespacedRegistry<I> makeShallowCopy() { return new FMLControlledNamespacedRegistry<I>(optionalDefaultKey, maxId, minId, superType); } }