/* * Minecraft Forge * Copyright (c) 2016. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation version 2.1 * of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ 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.Map.Entry; import java.util.Set; import com.google.common.base.Joiner; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import net.minecraftforge.event.RegistryEvent; import org.apache.commons.lang3.Validate; import org.apache.logging.log4j.Level; import net.minecraft.util.ResourceLocation; import net.minecraft.util.registry.RegistryNamespacedDefaultedByKey; import net.minecraftforge.fml.common.FMLLog; import net.minecraftforge.fml.common.functions.GenericIterableFactory; import net.minecraftforge.fml.common.registry.RegistryDelegate.Delegate; public class FMLControlledNamespacedRegistry> extends RegistryNamespacedDefaultedByKey implements IForgeRegistry { public static final boolean DEBUG = Boolean.parseBoolean(System.getProperty("fml.debugRegistryEntries", "false")); private final Class superType; private final boolean isDelegated; private ResourceLocation optionalDefaultKey; private I optionalDefaultObject; private int maxId; private int minId; /** * Aliases are resource location to resource location pointers, allowing for alternative names to be supplied * pointing at the same thing. They are used to allow programmatic migration of an ID. */ private final Map aliases = Maps.newHashMap(); /** * Persistent substitutions are the mechanism to allow mods to override specific behaviours with new behaviours. */ private final BiMap persistentSubstitutions = HashBiMap.create(); /** * Substitution originals - these are the originals that are being substituted */ private final BiMap substitutionOriginals = HashBiMap.create(); /** * This is the current active substitution set for a particular world. It will change as worlds come and go. */ private final BiMap activeSubstitutions = HashBiMap.create(); /** * The list of IDs blocked for this world. IDs will never be allocated in this set. */ private final Set blockedIds = Sets.newHashSet(); private final Set dummiedLocations = Sets.newHashSet(); private final BitSet availabilityMap; private final Map slaves = Maps.newHashMap(); private final AddCallback addCallback; private final ClearCallback clearCallback; private final CreateCallback createCallback; private final SubstitutionCallback substitutionCallback; FMLControlledNamespacedRegistry(ResourceLocation defaultKey, int minIdValue, int maxIdValue, Class type, BiMap> registries, AddCallback addCallback, ClearCallback clearCallback, CreateCallback createCallback, SubstitutionCallback substitutionCallback) { super(defaultKey); this.superType = type; this.optionalDefaultKey = defaultKey; this.minId = minIdValue; this.maxId = maxIdValue; this.availabilityMap = new BitSet(maxIdValue + 1); this.isDelegated = IForgeRegistryEntry.Impl.class.isAssignableFrom(type); this.addCallback = addCallback; this.clearCallback = clearCallback; this.createCallback = createCallback; this.substitutionCallback = substitutionCallback; if (createCallback != null) { createCallback.onCreate(slaves, registries); } } void validateContent(ResourceLocation registryName) { for (I obj : typeSafeIterable()) { int id = getId(obj); ResourceLocation name = getNameForObject(obj); boolean isSubstituted = activeSubstitutions.containsKey(name); // 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.", registryName, obj, id)); } // id lookup failed -> obj is not in the obj<->id map if (!isSubstituted && id < 0) { throw new IllegalStateException(String.format("Registry entry for %s %s, name %s, doesn't yield an id.", registryName, 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.", registryName, obj, name, id)); } // the rest of the tests don't really work for substituted items or blocks if (isSubstituted) { continue; } // 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, registryName, obj)); } // name -> obj lookup is inconsistent if (getRaw(name) != obj) { throw new IllegalStateException(String.format("Registry entry for name %s, id %d, doesn't yield the expected %s %s.", name, id, registryName, obj)); } // name -> id lookup is inconsistent if (getId(name) != id) { throw new IllegalStateException(String.format("Registry entry for name %s doesn't yield the expected id %d.", name, id)); } /* // 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.", registryName, obj, id, name)); } */ } } void set(FMLControlledNamespacedRegistry otherRegistry) { if (this.superType != otherRegistry.superType) { throw new IllegalArgumentException("incompatible registry"); } final Map slaves = Maps.newHashMap(this.slaves); slaves.put(PersistentRegistryManager.SUBSTITUTION_ORIGINALS, substitutionOriginals); if (this.clearCallback!=null) this.clearCallback.onClear(this, slaves); this.optionalDefaultKey = otherRegistry.optionalDefaultKey; this.maxId = otherRegistry.maxId; this.minId = otherRegistry.minId; this.aliases.clear(); this.aliases.putAll(otherRegistry.aliases); this.persistentSubstitutions.clear(); this.persistentSubstitutions.putAll(otherRegistry.getPersistentSubstitutions()); this.activeSubstitutions.clear(); this.dummiedLocations.clear(); this.dummiedLocations.addAll(otherRegistry.dummiedLocations); underlyingIntegerMap.clear(); registryObjects.clear(); for (I thing : otherRegistry.typeSafeIterable()) { addObjectRaw(otherRegistry.getId(thing), otherRegistry.getNameForObject(thing), thing); } for (ResourceLocation resloc : otherRegistry.activeSubstitutions.keySet()) { activateSubstitution(resloc); } this.substitutionOriginals.clear(); this.substitutionOriginals.putAll(otherRegistry.substitutionOriginals); } // public api /** * Add an object to the registry, trying to use the specified id. * This is required, to re-route vanilla block and item registration through to * the {@link #add} method. * * @deprecated register through {@link GameRegistry} instead. */ @Override @Deprecated public void register(int id, ResourceLocation name, I thing) { add(id, name, thing); } /** * DANGEROUS! EVIL! DO NOT USE! * * @deprecated register through {@link GameRegistry} instead. */ @Override @Deprecated public void putObject(ResourceLocation name, I thing) { if (name == null) { throw new NullPointerException("Can't use a null-name for the registry."); } if (thing == null) { throw new NullPointerException("Can't add null-object to the registry."); } ResourceLocation 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); } } /** * 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(ResourceLocation name) { I object = getRaw(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; } /** * 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 getIDForObjectBypass(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 super.getObjectById(id); } /** * 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 name) { I ret = super.getObject(name); if (ret == null) // no match, try aliases recursively { name = aliases.get(name); 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(ResourceLocation 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(ResourceLocation itemName) { I obj = getRaw(itemName); if (obj == null) { return -1; } return getId(obj); } /* * This iterator is used by FML to visit the actual block sets, it should use the super.iterator method instead * Compare #iterator() */ public Iterable typeSafeIterable() { return GenericIterableFactory.newCastingIterable(super.iterator(), superType); } // internal public void serializeIds(Map idMapping) // for saving { for (I thing : this.typeSafeIterable()) { idMapping.put(getNameForObject(thing), getId(thing)); } } public void serializeAliases(Map map) { map.putAll(this.aliases); } public void serializeSubstitutions(Set set) { set.addAll(activeSubstitutions.keySet()); } public void serializeDummied(Set set) { set.addAll(this.dummiedLocations); } public boolean isDummied(ResourceLocation key){ return this.dummiedLocations.contains(key); } /** * 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. * @return ID eventually allocated. */ int add(int id, ResourceLocation name, I thing) { if (name == null) { throw new NullPointerException(String.format("Can't use a null-name for the registry, object %s.", thing)); } if (thing == null) { throw new NullPointerException(String.format("Can't add null-object to the registry, name %s.", name)); } if (optionalDefaultKey != null && optionalDefaultKey.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); I 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 (PersistentRegistryManager.isFrozen(this)) { FMLLog.bigWarning("The object %s (name %s) is being added too late.", thing, name); } if (activeSubstitutions.containsKey(name)) { I oldThing = thing; thing = activeSubstitutions.get(name); if (DEBUG) { FMLLog.getLogger().log(Level.DEBUG, "Active substitution: {} {}@{} -> {}@{}", name, oldThing.getClass().getName(), System.identityHashCode(oldThing), thing.getClass().getName(), System.identityHashCode(thing)); } } addObjectRaw(idToUse, name, thing); if (isDelegated) { getExistingDelegate(thing).setName(name); } if (this.dummiedLocations.remove(name) && DEBUG) { FMLLog.fine("Registry Dummy Remove: %s", name); } if (DEBUG) { FMLLog.finer("Registry add: %s %d %s (req. id %d)", name, idToUse, thing, id); } return idToUse; } void markDummy(ResourceLocation rl, Integer id, I thing) { if (DEBUG) { FMLLog.finer("Registry Dummy Add: %s %d -> %s", rl, id, thing); } this.dummiedLocations.add(rl); this.addObjectRaw(id, rl, thing); } void addAlias(ResourceLocation from, ResourceLocation to) { aliases.put(from, to); if (DEBUG) { FMLLog.finer("Registry alias: %s -> %s", from, to); } } Map getEntriesNotIn(FMLControlledNamespacedRegistry registry) { Map ret = new HashMap(); for (I thing : this.typeSafeIterable()) { if (!registry.inverseObjectRegistry.containsKey(thing)) { if (!registry.activeSubstitutions.containsKey(getNameForObject(thing))) { ret.put(getNameForObject(thing), getId(thing)); } } } return ret; } void dump(ResourceLocation registryName) { if (!DEBUG) { return; } List ids = new ArrayList(); for (I thing : this.typeSafeIterable()) { ids.add(getId(thing)); } // sort by id Collections.sort(ids); FMLLog.finer("Registry Name : {}", registryName); for (int id : ids) { I thing = getRaw(id); FMLLog.finer("Registry: %d %s %s", id, getNameForObject(thing), thing); } } /** * Version of addObject not using the API restricting overrides. */ private void addObjectRaw(int id, ResourceLocation 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 availabilityMap.set(id); if (addCallback != null) { addCallback.onAdd(thing, id, slaves); } } public I getDefaultValue() { return optionalDefaultObject; } public RegistryDelegate getDelegate(I thing, Class clazz) { return new RegistryDelegate.Delegate(thing, clazz); } @SuppressWarnings("unchecked") private Delegate getExistingDelegate(I thing) { if (isDelegated) { return (Delegate)((IForgeRegistryEntry.Impl)thing).delegate; } else { return null; } } I activateSubstitution(ResourceLocation nameToReplace) { if (getPersistentSubstitutions().containsKey(nameToReplace)) { I original = getRaw(nameToReplace); I sub = getPersistentSubstitutions().get(nameToReplace); getExistingDelegate(original).changeReference(sub); activeSubstitutions.put(nameToReplace, sub); int id = getIDForObjectBypass(original); // force the new object into the existing map addObjectRaw(id, nameToReplace, sub); // Track the original in the substitution originals collection substitutionOriginals.put(nameToReplace, original); if (substitutionCallback!=null) substitutionCallback.onSubstituteActivated(slaves, original, sub, nameToReplace); return original; } return null; } void addSubstitutionAlias(String modId, ResourceLocation nameToReplace, I replacement) throws ExistingSubstitutionException { if (getPersistentSubstitutions().containsKey(nameToReplace) || getPersistentSubstitutions().containsValue(replacement)) { FMLLog.severe("The substitution of %s has already occurred. You cannot duplicate substitutions", nameToReplace); throw new ExistingSubstitutionException(nameToReplace, replacement); } 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"); } FMLLog.log(Level.DEBUG, "Adding substitution %s with %s (name %s)", original, replacement, nameToReplace); getPersistentSubstitutions().put(nameToReplace, replacement); } BiMap getPersistentSubstitutions() { return persistentSubstitutions; } @Override public void validateKey() { if (this.optionalDefaultKey != null) { Validate.notNull(this.optionalDefaultObject); } } /* * This iterator is used by some regular MC methods to visit all blocks, we need to include substitutions * Compare #typeSafeIterable() */ @Override public Iterator iterator() { return Iterators.concat(super.iterator(), getPersistentSubstitutions().values().iterator()); } FMLControlledNamespacedRegistry makeShallowCopy(BiMap> registries) { return new FMLControlledNamespacedRegistry(optionalDefaultKey, minId, maxId, superType, registries, addCallback, clearCallback, createCallback, substitutionCallback); } void resetSubstitutionDelegates() { if (!isDelegated) { return; } for (I obj : typeSafeIterable()) { Delegate delegate = getExistingDelegate(obj); delegate.changeReference(obj); } } @SuppressWarnings("unchecked") public > FMLControlledNamespacedRegistry asType(Class type) { return (FMLControlledNamespacedRegistry)this; } public void serializeBlockList(Set blocked) { blocked.addAll(this.blockedIds); } public Set getActiveSubstitutions() { return activeSubstitutions.keySet(); } public void loadAliases(Map aliases) { for (Map.Entry alias : aliases.entrySet()) { addAlias(alias.getKey(), alias.getValue()); } } public void loadSubstitutions(Set substitutions) { for (ResourceLocation rl : substitutions) { activateSubstitution(rl); } } public void loadBlocked(Set blocked) { for (Integer id : blocked) { blockedIds.add(id); availabilityMap.set(id); } } public void loadDummied(Set dummied) { if (DEBUG && dummied.size() > 0) { FMLLog.fine("Registry Dummy Load: [%s]", Joiner.on(", ").join(dummied)); } this.dummiedLocations.addAll(dummied); } public void loadIds(Map ids, Map missingIds, Map remappedIds, FMLControlledNamespacedRegistry currentRegistry, ResourceLocation registryName) { for (Map.Entry entry : ids.entrySet()) { ResourceLocation itemName = entry.getKey(); int newId = entry.getValue(); int currId = currentRegistry.getId(itemName); if (currId == -1) { FMLLog.info("Found a missing id from the world %s", itemName); missingIds.put(entry.getKey(), newId); continue; // no block/item -> nothing to add } else if (currId != newId) { FMLLog.fine("Fixed %s id mismatch %s: %d (init) -> %d (map).", registryName, itemName, currId, newId); remappedIds.put(itemName, new Integer[] {currId, newId}); } I obj = currentRegistry.getRaw(itemName); I sub = obj; // If we have an object in the originals set, we use that for initial adding - substitute activation will readd the substitute if neceessary later if (currentRegistry.substitutionOriginals.containsKey(itemName)) { obj = currentRegistry.substitutionOriginals.get(itemName); } add(newId, itemName, obj); if (currentRegistry.substitutionOriginals.containsKey(itemName) && substitutionCallback != null) { substitutionCallback.onSubstituteActivated(slaves, sub, obj, itemName); } } } public void blockId(int id) { blockedIds.add(id); } public void notifyCallbacks() { if (addCallback == null) return; for (I i : this.underlyingIntegerMap) { addCallback.onAdd(i, this.underlyingIntegerMap.add(i), slaves); } } @Override public ResourceLocation getNameForObject(I p_177774_1_) { ResourceLocation rl = super.getNameForObjectBypass(p_177774_1_); if (rl == null) { rl = activeSubstitutions.inverse().get(p_177774_1_); } return rl; } @Override public Class getRegistrySuperType() { return superType; } // IForgeRegistry: Modders should only interfaces with these methods @Override public void register(I value) { ResourceLocation key = value.getRegistryName(); if (key == null) { FMLLog.severe("Attempted to register a entry with a null name: %s", value); throw new NullPointerException(String.format("Attempted to register a entry with a null name: %s", value)); } add(-1, key, value); } public void registerAll(I... values) { for (I value: values) { register(value); } } @Override public boolean containsValue(I value) { return getKey(value) != null; } @Override public I getValue(ResourceLocation key) { return getObject(key); } @Override public ResourceLocation getKey(I value) { return getNameForObject(value); } @Override //Bouncer for OBF, as the super class's function is NotchCode and gets obfed. This plus the SRG lines prevents a AbstractMethodException public Set getKeys() { return super.getKeys(); } @Override public List getValues() { return Lists.newArrayList(this.iterator()); } @Override public Set> getEntries() { return Sets.newHashSet(new Iterator>() { Iterator itr = FMLControlledNamespacedRegistry.this.iterator(); @Override public boolean hasNext() { return itr.hasNext(); } @Override public Entry next() { final I value = itr.next(); final ResourceLocation key = FMLControlledNamespacedRegistry.this.getKey(value); return new Entry() { @Override public ResourceLocation getKey() { return key; } @Override public I getValue() { return value; } @Override public I setValue(I value) { throw new UnsupportedOperationException("Setting the value in an iterator is not allowed"); } }; } @Override public void remove() { throw new UnsupportedOperationException("This is a READ ONLY view of this registry."); } }); } @SuppressWarnings("unchecked") @Override public T getSlaveMap(ResourceLocation slaveMapName, Class type) { return (T)slaves.get(slaveMapName); } RegistryEvent.Register buildRegistryRegisterEvent(ResourceLocation location) { return new RegistryEvent.Register(location, this); } }