/*
* Forge Mod Loader
* Copyright (c) 2012-2013 cpw.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v2.1
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*
* Contributors:
* cpw - implementation
*/
package net.minecraftforge.fml.common.registry;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import net.minecraft.item.Item;
import net.minecraft.item.ItemBanner;
import net.minecraft.item.ItemBlock;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ObjectIntIdentityMap;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.Loader;
import net.minecraftforge.fml.common.ModContainer;
import net.minecraftforge.fml.common.StartupQuery;
import net.minecraftforge.fml.common.ZipperUtil;
import net.minecraftforge.fml.common.event.FMLMissingMappingsEvent;
import net.minecraftforge.fml.common.event.FMLMissingMappingsEvent.MissingMapping;
import net.minecraftforge.fml.common.registry.GameRegistry.Type;
import net.minecraftforge.fml.common.registry.GameRegistry.UniqueIdentifier;
import org.apache.logging.log4j.Level;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Joiner.MapJoiner;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
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 com.google.common.collect.Sets.SetView;
import com.google.common.collect.Table;
import com.google.common.io.Files;
public class GameData {
static final int MIN_BLOCK_ID = 0;
static final int MAX_BLOCK_ID = 4095;
static final int MIN_ITEM_ID = 4096;
static final int MAX_ITEM_ID = 31999;
private static final GameData mainData = new GameData();
private static Map<UniqueIdentifier, ModContainer> customOwners = Maps.newHashMap();
private static GameData frozen;
// public api
/**
* Get the currently active block registry.
*
* @return Block Registry.
*/
public static FMLControlledNamespacedRegistry<Block> getBlockRegistry() {
return getMain().iBlockRegistry;
}
/**
* Get the currently active item registry.
*
* @return Item Registry.
*/
public static FMLControlledNamespacedRegistry<Item> getItemRegistry() {
return getMain().iItemRegistry;
}
/***************************************************
* INTERNAL CODE FROM HERE ON DO NOT USE!
***************************************************/
public static class GameDataSnapshot
{
public static class Entry
{
public final Map<String, Integer> ids;
public final Set<String> substitutions;
public final Map<String, String> aliases;
public final Set<Integer> blocked;
public Entry()
{
this(new HashMap<String, Integer>(), new HashSet<String>(), new HashMap<String, String>(), new HashSet<Integer>());
}
public Entry(Map<String, Integer> ids, Set<String> substitions, Map<String, String> aliases, Set<Integer> blocked)
{
this.ids = ids;
this.substitutions = substitions;
this.aliases = aliases;
this.blocked = blocked;
}
public Entry(FMLControlledNamespacedRegistry registry)
{
this.ids = Maps.newHashMap();
this.substitutions = Sets.newHashSet();
this.aliases = Maps.newHashMap();
this.blocked = Sets.newHashSet();
registry.serializeInto(this.ids);
registry.serializeSubstitutions(this.substitutions);
registry.serializeAliases(this.aliases);
if (GameData.getBlockRegistry() == registry ||
GameData.getItemRegistry() == registry)
{
this.blocked.addAll(GameData.getMain().blockedIds);
}
}
}
public final Map<String, Entry> entries = Maps.newHashMap();
}
public static GameDataSnapshot takeSnapshot()
{
GameDataSnapshot snap = new GameDataSnapshot();
snap.entries.put("fml:blocks", new GameDataSnapshot.Entry(getMain().getBlockRegistry()));
snap.entries.put("fml:items", new GameDataSnapshot.Entry(getMain().getItemRegistry()));
for (Map.Entry<String, FMLControlledNamespacedRegistry<?>> e : getMain().genericRegistries.entrySet()) {
snap.entries.put("fmlgr:"+e.getKey(), new GameDataSnapshot.Entry(e.getValue()));
}
return snap;
}
static Item findItem(String modId, String name)
{
return (Item)getMain().iItemRegistry.getObject(modId + ":" + name);
}
static Block findBlock(String modId, String name)
{
String key = modId + ":" + name;
return getMain().iBlockRegistry.containsKey(key) ? getMain().iBlockRegistry.getObject(key) : null;
}
static UniqueIdentifier getUniqueName(Block block)
{
if (block == null) return null;
Object name = getMain().iBlockRegistry.getNameForObject(block);
return new UniqueIdentifier(name);
}
static UniqueIdentifier getUniqueName(Item item)
{
if (item == null) return null;
Object name = getMain().iItemRegistry.getNameForObject(item);
return new UniqueIdentifier(name);
}
/**
* Fix IDs improperly allocated by early versions of the registry, best-effort.
*
* Items sharing the same ID with a block, but not sharing the same registry name will be
* mapped to an unused id. Losing items instead of blocks should minimize the damage.
*
* @param dataList List containing the IDs to fix
*/
public static void fixBrokenIds(GameDataSnapshot.Entry blocks, GameDataSnapshot.Entry items, Set<Integer> blockedIds)
{
BitSet availabilityMap = new BitSet(MAX_ITEM_ID + 1);
// reserve all ids occupied by blocks
for (Entry<String, Integer> entry : blocks.ids.entrySet())
{
availabilityMap.set(entry.getValue());
}
Set<Integer> newBlockedIds = new HashSet<Integer>();
Set<String> itemsToRemove = new HashSet<String>();
Map<String, Integer> itemsToRelocate = new HashMap<String, Integer>();
// check all ids occupied by items
for (Entry<String, Integer> entry : items.ids.entrySet())
{
int oldId = entry.getValue();
String name = entry.getKey();
Item item = getMain().iItemRegistry.getRaw(name);
boolean blockThisId = false; // block oldId unless it's used by a block
if (item == null) // item no longer available
{
// can't fix items without reliably checking if they are ItemBlocks
FMLLog.warning("Item %s (old id %d) is no longer available and thus can't be fixed.", name, oldId);
itemsToRemove.add(name);
blockThisId = true;
}
else if (item instanceof ItemBlock)
{
if (blocks.ids.containsKey(name)) // the item was an ItemBlock before
{
int blockId = blocks.ids.get(name);
if (blockId != oldId) // mis-located ItemBlock
{
// relocate to the matching block
FMLLog.warning("ItemBlock %s (old id %d) doesn't have the same id as its block (%d).", name, oldId, blockId);
itemsToRelocate.put(name, blockId);
blockThisId = true;
}
else // intact ItemBlock
{
availabilityMap.set(oldId); // occupy id
}
}
else // the item hasn't been an ItemBlock before, but it's now
{
// can't fix these, drop them
FMLLog.warning("Item %s (old id %d) has been migrated to an ItemBlock and can't be fixed.", name, oldId);
itemsToRemove.add(name);
blockThisId = true;
}
}
else if (availabilityMap.get(oldId)) // normal item, id is already occupied
{
// remove the item mapping
FMLLog.warning("Item %s (old id %d) is conflicting with another block/item and can't be fixed.", name, oldId);
itemsToRemove.add(name);
}
else // intact Item
{
availabilityMap.set(oldId); // occupy id
}
// handle blocking the id from future use if possible (i.e. not used by a conflicting block)
// blockThisId requests don't modify availabilityMap, it could only be set by a block (or another item, which isn't being handled)
if (blockThisId && !availabilityMap.get(oldId))
{
// there's no block occupying this id, thus block the id from future use
// as there may still be ItemStacks in the world referencing it
newBlockedIds.add(oldId);
availabilityMap.set(oldId);
}
}
if (itemsToRemove.isEmpty() && itemsToRelocate.isEmpty()) return; // nothing to do
// confirm
String text = "Forge Mod Loader detected that this save is damaged.\n\n" +
"It's likely that an automatic repair can successfully restore\n" +
"most of it, except some items which may get swapped with others.\n\n" +
"A world backup will be created as a zip file in your saves\n" +
"directory automatically.\n\n" +
itemsToRemove.size()+" items need to be removed.\n"+
itemsToRelocate.size()+" items need to be relocated.";
boolean confirmed = StartupQuery.confirm(text);
if (!confirmed) StartupQuery.abort();
// confirm missing mods causing item removal
Set<String> modsMissing = new HashSet<String>();
for (String name : itemsToRemove)
{
modsMissing.add(name.substring(0, name.indexOf(':')));
}
for (Iterator<String> it = modsMissing.iterator(); it.hasNext(); )
{
String mod = it.next();
if (mod.equals("minecraft") || Loader.isModLoaded(mod)) it.remove();
}
if (!modsMissing.isEmpty())
{
text = "Forge Mod Loader detected that "+modsMissing.size()+" mods are missing.\n\n" +
"If you continue items previously provided by those mods will be\n" +
"removed while repairing this world save.\n\n" +
"Missing mods:\n";
for (String mod : modsMissing) text += mod+"\n";
confirmed = StartupQuery.confirm(text);
if (!confirmed) StartupQuery.abort();
}
// backup
try
{
String skip = System.getProperty("fml.doNotBackup");
if (skip == null || !"true".equals(skip))
{
ZipperUtil.backupWorld();
}
else
{
for (int x = 0; x < 10; x++)
FMLLog.severe("!!!!!!!!!! UPDATING WORLD WITHOUT DOING BACKUP !!!!!!!!!!!!!!!!");
}
}
catch (IOException e)
{
StartupQuery.notify("The world backup couldn't be created.\n\n"+e);
StartupQuery.abort();
}
// apply fix
for (String name : itemsToRemove)
{
FMLLog.warning("Removed Item %s, old id %d.", name, items.ids.remove(name));
}
for (Map.Entry<String, Integer> entry : itemsToRelocate.entrySet())
{
int newId = entry.getValue();
int oldId = items.ids.put(entry.getKey(), newId);
FMLLog.warning("Remapped Item %s to id %d, old id %d.", entry.getKey(), newId, oldId);
}
blockedIds.addAll(newBlockedIds);
}
public static List<String> injectSnapshot(GameDataSnapshot snapshot, boolean injectFrozenData, boolean isLocalWorld)
{
FMLLog.info("Injecting existing block and item data into this %s instance", FMLCommonHandler.instance().getEffectiveSide().isServer() ? "server" : "client");
Map<String, Integer[]> remapBlocks = Maps.newHashMap();
Map<String, Integer[]> remapItems = Maps.newHashMap();
LinkedHashMap<String, Integer> missingBlocks = new LinkedHashMap<String, Integer>();
LinkedHashMap<String, Integer> missingItems = new LinkedHashMap<String, Integer>();
getMain().testConsistency();
getMain().iBlockRegistry.dump();
getMain().iItemRegistry.dump();
GameDataSnapshot.Entry blocks = snapshot.entries.get("fml:blocks");
GameDataSnapshot.Entry items = snapshot.entries.get("fml:items");
GameData newData = new GameData();
for (int id : blocks.blocked)
{
newData.block(id);
}
for (Map.Entry<String, String> entry : blocks.aliases.entrySet())
{
newData.iBlockRegistry.addAlias(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, String> entry : items.aliases.entrySet())
{
newData.iItemRegistry.addAlias(entry.getKey(), entry.getValue());
}
for (String entry : blocks.substitutions)
{
newData.iBlockRegistry.activateSubstitution(entry);
}
for (String entry : items.substitutions)
{
newData.iItemRegistry.activateSubstitution(entry);
}
if (injectFrozenData)
{
for (String newBlockSubstitution : getMain().blockSubstitutions.keySet())
{
if (!blocks.substitutions.contains(newBlockSubstitution))
{
newData.iBlockRegistry.activateSubstitution(newBlockSubstitution);
}
}
for (String newItemSubstitution : getMain().itemSubstitutions.keySet())
{
if (!items.substitutions.contains(newItemSubstitution))
{
newData.iItemRegistry.activateSubstitution(newItemSubstitution);
}
}
}
// Clear State map for it's ready for us to register below.
GameData.BLOCKSTATE_TO_ID.clear();
// process blocks and items in the world, blocks in the first pass, items in the second
// blocks need to be added first for proper ItemBlock handling
for (int pass = 0; pass < 2; pass++)
{
boolean isBlock = (pass == 0);
Map<String, Integer> ids = (isBlock ? blocks.ids : items.ids);
for (Entry<String, Integer> entry : ids.entrySet())
{
String itemName = entry.getKey();
int newId = entry.getValue();
int currId = isBlock ? getMain().iBlockRegistry.getId(itemName) : getMain().iItemRegistry.getId(itemName);
if (currId == -1)
{
FMLLog.info("Found a missing id from the world %s", itemName);
(isBlock ? missingBlocks : missingItems).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).", isBlock ? "block" : "item", itemName, currId, newId);
(isBlock ? remapBlocks : remapItems).put(itemName, new Integer[] { currId, newId });
}
// register
if (isBlock)
{
currId = newData.registerBlock(getMain().iBlockRegistry.getRaw(itemName), itemName, newId);
}
else
{
currId = newData.registerItem(getMain().iItemRegistry.getRaw(itemName), itemName, newId);
}
if (currId != newId)
{
throw new IllegalStateException(String.format("Can't map %s %s to id %d (seen at: %d), already occupied by %s, blocked %b, ItemBlock %b",
isBlock ? "block" : "item",
itemName,
newId,
currId,
isBlock ? newData.iBlockRegistry.getRaw(newId) : newData.iItemRegistry.getRaw(newId),
newData.blockedIds.contains(newId),
isBlock ? false : (getMain().iItemRegistry.getRaw(currId) instanceof ItemBlock)));
}
}
}
List<String> missedMappings = Loader.instance().fireMissingMappingEvent(missingBlocks, missingItems, isLocalWorld, newData, remapBlocks, remapItems);
if (!missedMappings.isEmpty()) return missedMappings;
// If we got here - the load was accepted. We'll load generic repositories here.
// Generic registries can fail by returning a missing mapping.
missedMappings = newData.loadGenericRegistries(snapshot, getMain());
if (!missedMappings.isEmpty()) return missedMappings;
if (injectFrozenData) // add blocks + items missing from the map
{
Map<String, Integer> newBlocks = frozen.iBlockRegistry.getEntriesNotIn(newData.iBlockRegistry);
Map<String, Integer> newItems = frozen.iItemRegistry.getEntriesNotIn(newData.iItemRegistry);
if (!newBlocks.isEmpty() || !newItems.isEmpty())
{
FMLLog.info("Injecting new block and item data into this server instance.");
for (int pass = 0; pass < 2; pass++)
{
boolean isBlock = pass == 0;
Map<String, Integer> missing = (pass == 0) ? newBlocks : newItems;
Map<String, Integer[]> remaps = (isBlock ? remapBlocks : remapItems);
for (Entry<String, Integer> entry : missing.entrySet())
{
String itemName = entry.getKey();
int currId = entry.getValue();
int newId;
if (isBlock)
{
newId = newData.registerBlock(frozen.iBlockRegistry.getRaw(itemName), itemName, currId);
}
else
{
newId = newData.registerItem(frozen.iItemRegistry.getRaw(itemName), itemName, currId);
}
FMLLog.info("Injected new block/item %s: %d (init) -> %d (map).", itemName, currId, newId);
if (newId != currId) // a new id was assigned
{
remaps.put(itemName, new Integer[] { entry.getValue(), newId });
}
}
}
}
}
newData.testConsistency();
getMain().set(newData);
getMain().iBlockRegistry.dump();
getMain().iItemRegistry.dump();
Loader.instance().fireRemapEvent(remapBlocks, remapItems);
// The id map changed, ensure we apply object holders
ObjectHolderRegistry.INSTANCE.applyObjectHolders();
return ImmutableList.of();
}
public static List<String> processIdRematches(Iterable<MissingMapping> missedMappings, boolean isLocalWorld, GameData gameData, Map<String, Integer[]> remapBlocks, Map<String, Integer[]> remapItems)
{
List<String> failed = Lists.newArrayList();
List<String> ignored = Lists.newArrayList();
List<String> warned = Lists.newArrayList();
List<String> defaulted = Lists.newArrayList();
for (MissingMapping remap : missedMappings)
{
FMLMissingMappingsEvent.Action action = remap.getAction();
if (action == FMLMissingMappingsEvent.Action.REMAP)
{
// block/item re-mapped, finish the registration with the new name/object, but the old id
int currId, newId;
String newName;
if (remap.type == Type.BLOCK)
{
currId = getMain().iBlockRegistry.getId((Block) remap.getTarget());
newName = getMain().iBlockRegistry.getNameForObject(remap.getTarget()).toString();
FMLLog.fine("The Block %s is being remapped to %s.", remap.name, newName);
newId = gameData.registerBlock((Block) remap.getTarget(), newName, remap.id);
gameData.iBlockRegistry.addAlias(remap.name, newName);
}
else
{
currId = getMain().iItemRegistry.getId((Item) remap.getTarget());
newName = getMain().iItemRegistry.getNameForObject(remap.getTarget()).toString();
FMLLog.fine("The Item %s is being remapped to %s.", remap.name, newName);
newId = gameData.registerItem((Item) remap.getTarget(), newName, remap.id);
gameData.iItemRegistry.addAlias(remap.name, newName);
}
if (newId != remap.id) throw new IllegalStateException();
if (currId != newId)
{
FMLLog.info("Fixed %s id mismatch %s: %d (init) -> %d (map).", remap.type == Type.BLOCK ? "block" : "item", newName, currId, newId);
(remap.type == Type.BLOCK ? remapBlocks : remapItems).put(newName, new Integer[] { currId, newId });
}
}
else if (action == FMLMissingMappingsEvent.Action.BLOCKONLY)
{
// Pulled out specifically so the block doesn't get reassigned a new ID just because it's
// Item block has gone away
FMLLog.fine("The ItemBlock %s is no longer present in the game. The residual block will remain", remap.name);
}
else
{
// block item missing, warn as requested and block the id
if (action == FMLMissingMappingsEvent.Action.DEFAULT)
{
defaulted.add(remap.name);
}
else if (action == FMLMissingMappingsEvent.Action.IGNORE)
{
ignored.add(remap.name);
}
else if (action == FMLMissingMappingsEvent.Action.FAIL)
{
failed.add(remap.name);
}
else if (action == FMLMissingMappingsEvent.Action.WARN)
{
warned.add(remap.name);
}
gameData.block(remap.id); // prevent the id from being reused later
}
}
if (!defaulted.isEmpty())
{
String text = "Forge Mod Loader detected missing blocks/items.\n\n" +
"There are "+defaulted.size()+" missing blocks and items in this save.\n" +
"If you continue the missing blocks/items will get removed.\n" +
"A world backup will be automatically created in your saves directory.\n\n" +
"Missing Blocks/Items:\n";
for (String s : defaulted) text += s + "\n";
boolean confirmed = StartupQuery.confirm(text);
if (!confirmed) StartupQuery.abort();
try
{
String skip = System.getProperty("fml.doNotBackup");
if (skip == null || !"true".equals(skip))
{
ZipperUtil.backupWorld();
}
else
{
for (int x = 0; x < 10; x++)
FMLLog.severe("!!!!!!!!!! UPDATING WORLD WITHOUT DOING BACKUP !!!!!!!!!!!!!!!!");
}
}
catch (IOException e)
{
StartupQuery.notify("The world backup couldn't be created.\n\n"+e);
StartupQuery.abort();
}
warned.addAll(defaulted);
}
if (!failed.isEmpty())
{
FMLLog.severe("This world contains blocks and items that refuse to be remapped. The world will not be loaded");
return failed;
}
if (!warned.isEmpty())
{
FMLLog.severe("This world contains block and item mappings that may cause world breakage");
return failed;
}
else if (!ignored.isEmpty())
{
FMLLog.fine("There were %d missing mappings that have been ignored", ignored.size());
}
return failed;
}
public static void freezeData()
{
FMLLog.fine("Freezing block and item id maps");
getMain().testConsistency();
frozen = new GameData(getMain());
frozen.testConsistency();
}
public static void revertToFrozen()
{
if (frozen == null)
{
FMLLog.warning("Can't revert to frozen GameData state without freezing first.");
}
else
{
FMLLog.fine("Reverting to frozen data state.");
getMain().set(frozen);
}
// the id mapping has reverted, ensure we sync up the object holders
ObjectHolderRegistry.INSTANCE.applyObjectHolders();
}
protected static boolean isFrozen(FMLControlledNamespacedRegistry<?> registry)
{
return frozen != null && (getMain().iBlockRegistry == registry || getMain().iItemRegistry == registry);
}
protected static GameData getMain()
{
return mainData;
}
// internal registry objects
private final FMLControlledNamespacedRegistry<Block> iBlockRegistry;
private final FMLControlledNamespacedRegistry<Item> iItemRegistry;
// bit set marking ids as occupied
private final BitSet availabilityMap;
// IDs previously allocated in a world, but now unmapped/dangling; prevents the IDs from being reused
private final Set<Integer> blockedIds;
private GameData()
{
iBlockRegistry = new FMLControlledNamespacedRegistry<Block>(new ResourceLocation("minecraft:air"), MAX_BLOCK_ID, MIN_BLOCK_ID, Block.class);
iItemRegistry = new FMLControlledNamespacedRegistry<Item>(null, MAX_ITEM_ID, MIN_ITEM_ID, Item.class);
availabilityMap = new BitSet(MAX_ITEM_ID + 1);
blockedIds = new HashSet<Integer>();
genericRegistries = new HashMap<String,FMLControlledNamespacedRegistry<?>>();
}
private GameData(GameData data)
{
this();
set(data);
}
private void set(GameData data)
{
iBlockRegistry.set(data.iBlockRegistry);
iItemRegistry.set(data.iItemRegistry);
availabilityMap.clear();
availabilityMap.or(data.availabilityMap);
blockedIds.clear();
blockedIds.addAll(data.blockedIds);
copyGenericRegistries(data);
}
private void copyGenericRegistries(GameData data) {
for (Map.Entry<String, FMLControlledNamespacedRegistry<?>> e : data.genericRegistries.entrySet()) {
FMLControlledNamespacedRegistry<?> orig = e.getValue();
FMLControlledNamespacedRegistry<?> copy = orig.makeShallowCopy();
// UGLY AS FUCK
copy.setFrom(orig);
genericRegistries.put(e.getKey(), copy);
}
}
int register(Object obj, String name, int idHint) // from FMLControlledNamespacedRegistry.addObject
{
if (obj instanceof Block)
{
// tolerate extra name prefixes here since mc does it as well
name = addPrefix(name);
return registerBlock((Block) obj, name, idHint);
}
else if (obj instanceof Item)
{
// tolerate extra name prefixes here since mc does it as well
name = addPrefix(name);
return registerItem((Item) obj, name, idHint);
}
else
{
return findRegistry(obj.getClass()).add(idHint,name,obj);
}
}
int registerItem(Item item, String name) // from GameRegistry
{
int index = name.indexOf(':');
if (name.indexOf(':') != -1) FMLLog.bigWarning("Illegal extra prefix %s for name %s, invalid registry invocation/invalid name?", name.substring(0, index), name);
name = addPrefix(name);
return registerItem(item, name, -1);
}
private int registerItem(Item item, String name, int idHint)
{
if (item instanceof ItemBlock && !(item instanceof ItemBanner)) // ItemBlock, adjust id and clear the slot already occupied by the corresponding block
{
Block block = ((ItemBlock) item).block;
if (idHint != -1 && getMain().blockSubstitutions.containsKey(name))
{
block = getMain().blockSubstitutions.get(name);
}
int id = iBlockRegistry.getId(block);
if (id == -1) // ItemBlock before its Block
{
if (idHint < 0 || availabilityMap.get(idHint) || idHint > MAX_BLOCK_ID) // non-suitable id, allocate one in the block id range, add would use the item id range otherwise
{
id = availabilityMap.nextClearBit(MIN_BLOCK_ID); // find suitable id here, iItemRegistry would search from MIN_ITEM_ID
if (id > MAX_BLOCK_ID) throw new RuntimeException(String.format("Invalid id %d - maximum id range exceeded.", id));
FMLLog.fine("Allocated id %d for ItemBlock %s in the block id range, original id requested: %d.", id, name, idHint);
}
else // idHint is suitable without changes
{
id = idHint;
}
}
else // ItemBlock after its Block
{
FMLLog.fine("Found matching Block %s for ItemBlock %s at id %d, original id requested: %d", block, item, id, idHint);
freeSlot(id, item); // temporarily free the slot occupied by the Block for the item registration
}
idHint = id;
}
int itemId = iItemRegistry.add(idHint, name, item, availabilityMap);
if (item instanceof ItemBlock) // verify
{
if (itemId != idHint) throw new IllegalStateException(String.format("ItemBlock at block id %d insertion failed, got id %d.", idHint, itemId));
verifyItemBlockName((ItemBlock) item);
}
// block the Block Registry slot with the same id
useSlot(itemId);
((RegistryDelegate.Delegate<Item>) item.delegate).setName(name);
return itemId;
}
int registerBlock(Block block, String name) // from GameRegistry
{
int index = name.indexOf(':');
if (name.indexOf(':') != -1) FMLLog.bigWarning("Illegal extra prefix %s for name %s, invalid registry invocation/invalid name?", name.substring(0, index), name);
name = addPrefix(name);
return registerBlock(block, name, -1);
}
private int registerBlock(Block block, String name, int idHint)
{
// handle ItemBlock-before-Block registrations
ItemBlock itemBlock = null;
for (Item item : iItemRegistry.typeSafeIterable()) // find matching ItemBlock
{
if (item instanceof ItemBlock && ((ItemBlock) item).block == block)
{
itemBlock = (ItemBlock) item;
break;
}
}
if (itemBlock != null) // has ItemBlock, adjust id and clear the slot already occupied by the corresponding item
{
idHint = iItemRegistry.getId(itemBlock);
FMLLog.fine("Found matching ItemBlock %s for Block %s at id %d", itemBlock, block, idHint);
freeSlot(idHint, block); // temporarily free the slot occupied by the Item for the block registration
}
// add
int blockId = iBlockRegistry.add(idHint, name, block, availabilityMap);
if (itemBlock != null) // verify
{
if (blockId != idHint) throw new IllegalStateException(String.format("Block at itemblock id %d insertion failed, got id %d.", idHint, blockId));
verifyItemBlockName(itemBlock);
}
useSlot(blockId);
((RegistryDelegate.Delegate<Block>) block.delegate).setName(name);
for (IBlockState state : ((List<IBlockState>)block.getBlockState().getValidStates()))
{
GameData.BLOCKSTATE_TO_ID.put(state, blockId << 4 | block.getMetaFromState(state));
}
return blockId;
}
/**
* Block the specified id from being reused.
*/
private void block(int id)
{
blockedIds.add(id);
useSlot(id);
}
private void useSlot(int id)
{
availabilityMap.set(id);
}
/**
* Free the specified slot.
*
* The slot must not be occupied by something else than the specified object within the same type.
* The same object is permitted for handling duplicate registrations.
*
* @param id id to free
* @param obj object allowed besides different types (block vs item)
*/
private void freeSlot(int id, Object obj)
{
FMLControlledNamespacedRegistry<?> registry = (obj instanceof Block) ? iBlockRegistry : iItemRegistry;
Object thing = registry.getRaw(id);
if (thing != null && thing != obj)
{
throw new IllegalStateException(String.format("Can't free registry slot %d occupied by %s", id, thing));
}
availabilityMap.clear(id);
}
/**
* Prefix the supplied name with the current mod id.
*
* If no mod id can be determined, minecraft will be assumed.
* The prefix is separated with a colon.
*
* If there's already a prefix, it'll be prefixed again if the new prefix
* doesn't match the old prefix, as used by vanilla calls to addObject.
*
* @param name name to prefix.
* @return prefixed name.
*/
private String addPrefix(String name)
{
int index = name.lastIndexOf(':');
String oldPrefix = index == -1 ? "" : name.substring(0, index);
String prefix;
ModContainer mc = Loader.instance().activeModContainer();
if (mc != null)
{
prefix = mc.getModId();
}
else // no mod container, assume minecraft
{
prefix = "minecraft";
}
if (!oldPrefix.equals(prefix))
{
name = prefix + ":" + name;
}
return name;
}
private void verifyItemBlockName(ItemBlock item)
{
Object blockName = iBlockRegistry.getNameForObject(item.block);
Object itemName = iItemRegistry.getNameForObject(item);
//Vanilla has a mismatch:
//Block <-> ItemBlock name mismatch, block name minecraft:standing_banner, item name minecraft:banner
//TODO: Untie these in the rest of the registry
if (blockName != null && !blockName.equals(itemName) && !"minecraft:standing_banner".equals(blockName.toString()))
{
FMLLog.bigWarning("Block <-> ItemBlock name mismatch, block name %s, item name %s", blockName, itemName);
}
}
private void testConsistency() {
// test if there's an entry for every set bit in availabilityMap
for (int i = availabilityMap.nextSetBit(0); i >= 0; i = availabilityMap.nextSetBit(i+1))
{
if (iBlockRegistry.getRaw(i) == null && iItemRegistry.getRaw(i) == null && !blockedIds.contains(i))
{
throw new IllegalStateException(String.format("availabilityMap references empty entries for id %d.", i));
}
}
for (int pass = 0; pass < 2; pass++)
{
boolean isBlock = pass == 0;
String type = isBlock ? "block" : "item";
FMLControlledNamespacedRegistry<?> registry = isBlock ? iBlockRegistry : iItemRegistry;
registry.validateContent((isBlock ? MAX_BLOCK_ID : MAX_ITEM_ID), type, availabilityMap, blockedIds, iBlockRegistry);
}
FMLLog.fine("Registry consistency check successful");
}
void registerSubstitutionAlias(String nameToSubstitute, Type type, Object toReplace) throws ExistingSubstitutionException
{
type.getRegistry().addSubstitutionAlias(Loader.instance().activeModContainer().getModId(),nameToSubstitute, toReplace);
type.getRegistry().activateSubstitution(nameToSubstitute);
}
static <T> RegistryDelegate<T> buildDelegate(T referant, Class<T> type)
{
return new RegistryDelegate.Delegate<T>(referant, type);
}
private BiMap<String, Item> itemSubstitutions = HashBiMap.create();
private BiMap<String, Block> blockSubstitutions = HashBiMap.create();
@SuppressWarnings("unchecked")
<T> BiMap<String, T> getPersistentSubstitutionMap(Class<T> type)
{
if (type.equals(Item.class))
{
return (BiMap<String, T>) itemSubstitutions;
}
else if (type.equals(Block.class))
{
return (BiMap<String, T>) blockSubstitutions;
}
else
{
return ImmutableBiMap.of();
}
}
private static Map<Block, Item> BLOCK_TO_ITEM = Maps.newHashMap();
//Internal: DO NOT USE, will change without warning.
public static Map getBlockItemMap()
{
return BLOCK_TO_ITEM;
}
private static ClearableObjectIntIdentityMap BLOCKSTATE_TO_ID = new ClearableObjectIntIdentityMap();
//Internal: DO NOT USE, will change without warning.
public static ObjectIntIdentityMap getBlockStateIDMap()
{
return BLOCKSTATE_TO_ID;
}
//Lets us clear the map so we can rebuild it.
private static class ClearableObjectIntIdentityMap extends ObjectIntIdentityMap
{
private void clear()
{
this.identityMap.clear();
this.objectList.clear();
}
}
private Map<String,FMLControlledNamespacedRegistry<?>> genericRegistries;
// Seed registry types with the blocks and items types so you can't make a new registry of them
private BiMap<String,Class<?>> registryTypes = HashBiMap.create(
ImmutableMap.<String,Class<?>>builder()
.put("minecraft:blocks", Block.class)
.put("minecraft:items", Item.class).build());
private void findSuperTypes(Class<?> type, Set<Class<?>> types) {
if (type == Object.class) {
return;
}
types.add(type);
for (Class<?> intf : type.getInterfaces()) {
findSuperTypes(intf, types);
}
findSuperTypes(type.getSuperclass(),types);
}
@SuppressWarnings("unchecked")
private <T> FMLControlledNamespacedRegistry<T> getGenericRegistry(String registryName, Class<T> type) {
FMLControlledNamespacedRegistry<?> fmlControlledNamespacedRegistry = genericRegistries.get(registryName);
return (FMLControlledNamespacedRegistry<T>) fmlControlledNamespacedRegistry;
}
@SuppressWarnings("unchecked")
private <T> FMLControlledNamespacedRegistry<T> createGenericRegistry(String registryName, Class<T> type, int minId, int maxId) {
Set<Class<?>> parents = Sets.newHashSet();
findSuperTypes(type, parents);
SetView<Class<?>> overlappedTypes = Sets.intersection(parents, registryTypes.values());
if (!overlappedTypes.isEmpty()) {
Class<?> foundType = overlappedTypes.iterator().next();
FMLLog.severe("Found existing registry of type %1s named %2s, you cannot create a new registry (%3s) with type %4s, as %4s has a parent of that type", foundType, registryTypes.inverse().get(foundType), registryName, type);
throw new IllegalArgumentException("Duplicate registry parent type found - you can only have one registry for a particular super type");
}
FMLControlledNamespacedRegistry<?> fmlControlledNamespacedRegistry = new FMLControlledNamespacedRegistry<T>(null, maxId, minId, type);
genericRegistries.put(registryName, fmlControlledNamespacedRegistry);
registryTypes.put(registryName, type);
return (FMLControlledNamespacedRegistry<T>) fmlControlledNamespacedRegistry;
}
public static <T> FMLControlledNamespacedRegistry<T> createRegistry(String registryName, Class<T> type, int minId, int maxId) {
return getMain().createGenericRegistry(registryName, type, minId, maxId);
}
private FMLControlledNamespacedRegistry<?> findRegistry(Class<?> type) {
BiMap<Class<?>, String> typeReg = registryTypes.inverse();
String name = typeReg.get(type);
if (name == null) {
Set<Class<?>> parents = Sets.newHashSet();
findSuperTypes(type, parents);
SetView<Class<?>> foundType = Sets.intersection(parents, registryTypes.values());
if (foundType.isEmpty()) {
FMLLog.severe("Unable to find registry for type %s", type.getName());
throw new IllegalArgumentException("Attempt to register an object without an associated registry");
}
Class<?> regtype = Iterables.getOnlyElement(foundType);
name = typeReg.get(regtype);
}
return genericRegistries.get(name);
}
private List<String> loadGenericRegistries(GameDataSnapshot snapshot, GameData existing) {
List<String> result = Lists.newArrayList();
for (Map.Entry<String, FMLControlledNamespacedRegistry<?>> e : existing.genericRegistries.entrySet())
{
String regName = e.getKey();
FMLControlledNamespacedRegistry<?> registry = e.getValue();
FMLControlledNamespacedRegistry<?> newRegistry = genericRegistries.get(regName);
if (newRegistry == null)
{
newRegistry = registry.makeShallowCopy();
genericRegistries.put(regName, newRegistry);
}
GameDataSnapshot.Entry regSnap = snapshot.entries.get("fmlgr:"+regName);
if (regSnap == null) {
FMLLog.info("Weird, there was no registry data for registry %s found in the snapshot", regName);
continue;
}
for (Entry<String, Integer> entry : regSnap.ids.entrySet())
{
String entryName = entry.getKey();
int entryId = entry.getValue();
int currId = registry.getId(entryName);
if (currId == -1)
{
FMLLog.info("Found a missing id in registry %s from the world %s", regName, entryName);
result.add(regName+"{"+entryName+"}="+entryId);
continue; // no block/item -> nothing to add
}
else if (currId != entryId)
{
FMLLog.fine("Fixed registry %s id mismatch %s: %d (init) -> %d (map).", regName, entryName, currId, entryId);
}
newRegistry.add(entryId, entryName, registry.getRaw(entryName));
}
}
return result;
}
public static <T> FMLControlledNamespacedRegistry<T> getRegistry(String registryName, Class<T> type) {
return getMain().getGenericRegistry(registryName, type);
}
}