001/*
002 * Forge Mod Loader
003 * Copyright (c) 2012-2013 cpw.
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser Public License v2.1
006 * which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
008 *
009 * Contributors:
010 *     cpw - implementation
011 */
012
013package cpw.mods.fml.common.registry;
014
015import java.io.File;
016import java.io.FileInputStream;
017import java.io.FileNotFoundException;
018import java.io.IOException;
019import java.util.Map;
020import java.util.Properties;
021import java.util.Set;
022import java.util.concurrent.CountDownLatch;
023import java.util.logging.Level;
024
025import net.minecraft.block.Block;
026import net.minecraft.block.BlockSand;
027import net.minecraft.item.Item;
028import net.minecraft.item.ItemStack;
029import net.minecraft.nbt.NBTTagCompound;
030import net.minecraft.nbt.NBTTagList;
031
032import com.google.common.base.Charsets;
033import com.google.common.base.Function;
034import com.google.common.base.Joiner;
035import com.google.common.base.Throwables;
036import com.google.common.base.Joiner.MapJoiner;
037import com.google.common.collect.HashBasedTable;
038import com.google.common.collect.ImmutableListMultimap;
039import com.google.common.collect.ImmutableMap;
040import com.google.common.collect.ImmutableTable;
041import com.google.common.collect.ImmutableTable.Builder;
042import com.google.common.collect.MapDifference;
043import com.google.common.collect.Tables;
044import com.google.common.collect.MapDifference.ValueDifference;
045import com.google.common.collect.Maps;
046import com.google.common.collect.Sets;
047import com.google.common.collect.Table;
048import com.google.common.collect.Table.Cell;
049import com.google.common.io.Files;
050
051import cpw.mods.fml.common.FMLLog;
052import cpw.mods.fml.common.Loader;
053import cpw.mods.fml.common.LoaderState;
054import cpw.mods.fml.common.ModContainer;
055
056public class GameData {
057    private static Map<Integer, ItemData> idMap = Maps.newHashMap();
058    private static CountDownLatch serverValidationLatch;
059    private static CountDownLatch clientValidationLatch;
060    private static MapDifference<Integer, ItemData> difference;
061    private static boolean shouldContinue = true;
062    private static boolean isSaveValid = true;
063    private static ImmutableTable<String, String, Integer> modObjectTable;
064    private static Table<String, String, ItemStack> customItemStacks = HashBasedTable.create();
065    private static Map<String,String> ignoredMods;
066
067    private static boolean isModIgnoredForIdValidation(String modId)
068    {
069        if (ignoredMods == null)
070        {
071            File f = new File(Loader.instance().getConfigDir(),"fmlIDChecking.properties");
072            if (f.exists())
073            {
074                Properties p = new Properties();
075                try
076                {
077                    p.load(new FileInputStream(f));
078                    ignoredMods = Maps.fromProperties(p);
079                    if (ignoredMods.size()>0)
080                    {
081                        FMLLog.log("fml.ItemTracker", Level.WARNING, "Using non-empty ignored mods configuration file %s", ignoredMods.keySet());
082                    }
083                }
084                catch (Exception e)
085                {
086                    Throwables.propagateIfPossible(e);
087                    FMLLog.log("fml.ItemTracker", Level.SEVERE, e, "Failed to read ignored ID checker mods properties file");
088                    ignoredMods = ImmutableMap.<String, String>of();
089                }
090            }
091            else
092            {
093                ignoredMods = ImmutableMap.<String, String>of();
094            }
095        }
096        return ignoredMods.containsKey(modId);
097    }
098
099    public static void newItemAdded(Item item)
100    {
101        ModContainer mc = Loader.instance().activeModContainer();
102        if (mc == null)
103        {
104            mc = Loader.instance().getMinecraftModContainer();
105            if (Loader.instance().hasReachedState(LoaderState.AVAILABLE))
106            {
107                FMLLog.severe("It appears something has tried to allocate an Item outside of the initialization phase of Minecraft, this could be very bad for your network connectivity.");
108            }
109        }
110        String itemType = item.getClass().getName();
111        ItemData itemData = new ItemData(item, mc);
112        if (idMap.containsKey(item.itemID))
113        {
114            ItemData id = idMap.get(item.itemID);
115            FMLLog.log("fml.ItemTracker", Level.INFO, "The mod %s is overwriting existing item at %d (%s from %s) with %s", mc.getModId(), id.getItemId(), id.getItemType(), id.getModId(), itemType);
116        }
117        idMap.put(item.itemID, itemData);
118        if (!"Minecraft".equals(mc.getModId()))
119        {
120            FMLLog.log("fml.ItemTracker",Level.FINE, "Adding item %s(%d) owned by %s", item.getClass().getName(), item.itemID, mc.getModId());
121        }
122    }
123
124    public static void validateWorldSave(Set<ItemData> worldSaveItems)
125    {
126        isSaveValid = true;
127        shouldContinue = true;
128        // allow ourselves to continue if there's no saved data
129        if (worldSaveItems == null)
130        {
131            serverValidationLatch.countDown();
132            try
133            {
134                clientValidationLatch.await();
135            }
136            catch (InterruptedException e)
137            {
138            }
139            return;
140        }
141
142        Function<? super ItemData, Integer> idMapFunction = new Function<ItemData, Integer>() {
143            public Integer apply(ItemData input) {
144                return input.getItemId();
145            };
146        };
147
148        Map<Integer,ItemData> worldMap = Maps.uniqueIndex(worldSaveItems,idMapFunction);
149        difference = Maps.difference(worldMap, idMap);
150        FMLLog.log("fml.ItemTracker", Level.FINE, "The difference set is %s", difference);
151        if (!difference.entriesDiffering().isEmpty() || !difference.entriesOnlyOnLeft().isEmpty())
152        {
153            FMLLog.log("fml.ItemTracker", Level.SEVERE, "FML has detected item discrepancies");
154            FMLLog.log("fml.ItemTracker", Level.SEVERE, "Missing items : %s", difference.entriesOnlyOnLeft());
155            FMLLog.log("fml.ItemTracker", Level.SEVERE, "Mismatched items : %s", difference.entriesDiffering());
156            boolean foundNonIgnored = false;
157            for (ItemData diff : difference.entriesOnlyOnLeft().values())
158            {
159                if (!isModIgnoredForIdValidation(diff.getModId()))
160                {
161                    foundNonIgnored = true;
162                }
163            }
164            for (ValueDifference<ItemData> diff : difference.entriesDiffering().values())
165            {
166                if (! ( isModIgnoredForIdValidation(diff.leftValue().getModId()) || isModIgnoredForIdValidation(diff.rightValue().getModId()) ) )
167                {
168                    foundNonIgnored = true;
169                }
170            }
171            if (!foundNonIgnored)
172            {
173                FMLLog.log("fml.ItemTracker", Level.SEVERE, "FML is ignoring these ID discrepancies because of configuration. YOUR GAME WILL NOW PROBABLY CRASH. HOPEFULLY YOU WON'T HAVE CORRUPTED YOUR WORLD. BLAME %s", ignoredMods.keySet());
174            }
175            isSaveValid = !foundNonIgnored;
176            serverValidationLatch.countDown();
177        }
178        else
179        {
180            isSaveValid = true;
181            serverValidationLatch.countDown();
182        }
183        try
184        {
185            clientValidationLatch.await();
186            if (!shouldContinue)
187            {
188                throw new RuntimeException("This server instance is going to stop abnormally because of a fatal ID mismatch");
189            }
190        }
191        catch (InterruptedException e)
192        {
193        }
194    }
195
196    public static void writeItemData(NBTTagList itemList)
197    {
198        for (ItemData dat : idMap.values())
199        {
200            itemList.appendTag(dat.toNBT());
201        }
202    }
203
204    /**
205     * Initialize the server gate
206     * @param gateCount the countdown amount. If it's 2 we're on the client and the client and server
207     * will wait at the latch. 1 is a server and the server will proceed
208     */
209    public static void initializeServerGate(int gateCount)
210    {
211        serverValidationLatch = new CountDownLatch(gateCount - 1);
212        clientValidationLatch = new CountDownLatch(gateCount - 1);
213    }
214
215    public static MapDifference<Integer, ItemData> gateWorldLoadingForValidation()
216    {
217        try
218        {
219            serverValidationLatch.await();
220            if (!isSaveValid)
221            {
222                return difference;
223            }
224        }
225        catch (InterruptedException e)
226        {
227        }
228        difference = null;
229        return null;
230    }
231
232
233    public static void releaseGate(boolean carryOn)
234    {
235        shouldContinue = carryOn;
236        clientValidationLatch.countDown();
237    }
238
239    public static Set<ItemData> buildWorldItemData(NBTTagList modList)
240    {
241        Set<ItemData> worldSaveItems = Sets.newHashSet();
242        for (int i = 0; i < modList.tagCount(); i++)
243        {
244            NBTTagCompound mod = (NBTTagCompound) modList.tagAt(i);
245            ItemData dat = new ItemData(mod);
246            worldSaveItems.add(dat);
247        }
248        return worldSaveItems;
249    }
250
251    static void setName(Item item, String name, String modId)
252    {
253        int id = item.itemID;
254        ItemData itemData = idMap.get(id);
255        itemData.setName(name,modId);
256    }
257
258    public static void buildModObjectTable()
259    {
260        if (modObjectTable != null)
261        {
262            throw new IllegalStateException("Illegal call to buildModObjectTable!");
263        }
264
265        Map<Integer, Cell<String, String, Integer>> map = Maps.transformValues(idMap, new Function<ItemData,Cell<String,String,Integer>>() {
266            public Cell<String,String,Integer> apply(ItemData data)
267            {
268                if ("Minecraft".equals(data.getModId()) || !data.isOveridden())
269                {
270                    return null;
271                }
272                return Tables.immutableCell(data.getModId(), data.getItemType(), data.getItemId());
273            }
274        });
275
276        Builder<String, String, Integer> tBuilder = ImmutableTable.builder();
277        for (Cell<String, String, Integer> c : map.values())
278        {
279            if (c!=null)
280            {
281                tBuilder.put(c);
282            }
283        }
284        modObjectTable = tBuilder.build();
285    }
286    static Item findItem(String modId, String name)
287    {
288        if (modObjectTable == null || !modObjectTable.contains(modId, name))
289        {
290            return null;
291        }
292
293        return Item.itemsList[modObjectTable.get(modId, name)];
294    }
295
296    static Block findBlock(String modId, String name)
297    {
298        if (modObjectTable == null)
299        {
300            return null;
301        }
302
303        Integer blockId = modObjectTable.get(modId, name);
304        if (blockId == null || blockId >= Block.blocksList.length)
305        {
306            return null;
307        }
308        return Block.blocksList[blockId];
309    }
310
311    static ItemStack findItemStack(String modId, String name)
312    {
313        ItemStack is = customItemStacks.get(modId, name);
314        if (is == null)
315        {
316            Item i = findItem(modId, name);
317            if (i != null)
318            {
319                is = new ItemStack(i, 0 ,0);
320            }
321        }
322        if (is == null)
323        {
324            Block b = findBlock(modId, name);
325            if (b != null)
326            {
327                is = new ItemStack(b, 0, Short.MAX_VALUE);
328            }
329        }
330        return is;
331    }
332
333    static void registerCustomItemStack(String name, ItemStack itemStack)
334    {
335        customItemStacks.put(Loader.instance().activeModContainer().getModId(), name, itemStack);
336    }
337
338    public static void dumpRegistry(File minecraftDir)
339    {
340        if (customItemStacks == null)
341        {
342            return;
343        }
344        if (Boolean.valueOf(System.getProperty("fml.dumpRegistry", "false")).booleanValue())
345        {
346            ImmutableListMultimap.Builder<String, String> builder = ImmutableListMultimap.builder();
347            for (String modId : customItemStacks.rowKeySet())
348            {
349                builder.putAll(modId, customItemStacks.row(modId).keySet());
350            }
351
352            File f = new File(minecraftDir, "itemStackRegistry.csv");
353            MapJoiner mapJoiner = Joiner.on("\n").withKeyValueSeparator(",");
354            try
355            {
356                Files.write(mapJoiner.join(builder.build().entries()), f, Charsets.UTF_8);
357                FMLLog.log(Level.INFO, "Dumped item registry data to %s", f.getAbsolutePath());
358            }
359            catch (IOException e)
360            {
361                FMLLog.log(Level.SEVERE, e, "Failed to write registry data to %s", f.getAbsolutePath());
362            }
363        }
364    }
365}