    001/**
    002 * This software is provided under the terms of the Minecraft Forge Public
    003 * License v1.0.
    004 */
    005
    006package net.minecraftforge.common;
    007
    008import static net.minecraftforge.common.Property.Type.BOOLEAN;
    009import static net.minecraftforge.common.Property.Type.DOUBLE;
    010import static net.minecraftforge.common.Property.Type.INTEGER;
    011import static net.minecraftforge.common.Property.Type.STRING;
    012
    013import java.io.BufferedReader;
    014import java.io.BufferedWriter;
    015import java.io.File;
    016import java.io.FileInputStream;
    017import java.io.FileOutputStream;
    018import java.io.IOException;
    019import java.io.InputStream;
    020import java.io.InputStreamReader;
    021import java.io.OutputStreamWriter;
    022import java.io.PushbackInputStream;
    023import java.io.Reader;
    024import java.util.ArrayList;
    025import java.util.Arrays;
    026import java.util.Locale;
    027import java.util.Map;
    028import java.util.Set;
    029import java.util.TreeMap;
    030import java.util.regex.Matcher;
    031import java.util.regex.Pattern;
    032
    033import net.minecraft.block.Block;
    034import net.minecraft.item.Item;
    035
    036import com.google.common.base.CharMatcher;
    037import com.google.common.collect.ImmutableSet;
    038
    039import cpw.mods.fml.common.FMLLog;
    040import cpw.mods.fml.common.Loader;
    041import cpw.mods.fml.relauncher.FMLInjectionData;
    042
    043/**
    044 * This class offers advanced configurations capabilities, allowing to provide
    045 * various categories for configuration variables.
    046 */
    047public class Configuration
    048{
    049    private static boolean[] configMarkers = new boolean[Item.itemsList.length];
    050    private static final int ITEM_SHIFT = 256;
    051    private static final int MAX_BLOCKS = 4096;
    052
    053    public static final String CATEGORY_GENERAL = "general";
    054    public static final String CATEGORY_BLOCK   = "block";
    055    public static final String CATEGORY_ITEM    = "item";
    056    public static final String ALLOWED_CHARS = "._-";
    057    public static final String DEFAULT_ENCODING = "UTF-8";
    058    public static final String CATEGORY_SPLITTER = ".";
    059    public static final String NEW_LINE;
    060    private static final Pattern CONFIG_START = Pattern.compile("START: \"([^\\\"]+)\"");
    061    private static final Pattern CONFIG_END = Pattern.compile("END: \"([^\\\"]+)\"");
    062    public static final CharMatcher allowedProperties = CharMatcher.JAVA_LETTER_OR_DIGIT.or(CharMatcher.anyOf(ALLOWED_CHARS));
    063    private static Configuration PARENT = null;
    064
    065    File file;
    066
    067    private Map<String, ConfigCategory> categories = new TreeMap<String, ConfigCategory>();
    068    private Map<String, Configuration> children = new TreeMap<String, Configuration>();
    069
    070    private boolean caseSensitiveCustomCategories;
    071    public String defaultEncoding = DEFAULT_ENCODING;
    072    private String fileName = null;
    073    public boolean isChild = false;
    074    private boolean changed = false;
    075
    076    static
    077    {
    078        Arrays.fill(configMarkers, false);
    079        NEW_LINE = System.getProperty("line.separator");
    080    }
    081
    082    public Configuration(){}
    083
    084    /**
    085     * Create a configuration file for the file given in parameter.
    086     */
    087    public Configuration(File file)
    088    {
    089        this.file = file;
    090        String basePath = ((File)(FMLInjectionData.data()[6])).getAbsolutePath().replace(File.separatorChar, '/').replace("/.", "");
    091        String path = file.getAbsolutePath().replace(File.separatorChar, '/').replace("/./", "/").replace(basePath, "");
    092        if (PARENT != null)
    093        {
    094            PARENT.setChild(path, this);
    095            isChild = true;
    096        }
    097        else
    098        {
    099            fileName = path;
    100            load();
    101        }
    102    }
    103
    104    public Configuration(File file, boolean caseSensitiveCustomCategories)
    105    {
    106        this(file);
    107        this.caseSensitiveCustomCategories = caseSensitiveCustomCategories;
    108    }
    109
    110    /**
    111     * Gets or create a block id property. If the block id property key is
    112     * already in the configuration, then it will be used. Otherwise,
    113     * defaultId will be used, except if already taken, in which case this
    114     * will try to determine a free default id.
    115     */
    116    public Property getBlock(String key, int defaultID) { return getBlock(CATEGORY_BLOCK, key, defaultID, null); }
    117    public Property getBlock(String key, int defaultID, String comment) { return getBlock(CATEGORY_BLOCK, key, defaultID, comment); }
    118    public Property getBlock(String category, String key, int defaultID) { return getBlockInternal(category, key, defaultID, null, 256, Block.blocksList.length); }
    119    public Property getBlock(String category, String key, int defaultID, String comment) { return getBlockInternal(category, key, defaultID, comment, 256, Block.blocksList.length); }
    120
    121    /**
    122     * Special version of getBlock to be used when you want to garentee the ID you get is below 256
    123     * This should ONLY be used by mods who do low level terrain generation, or ones that add new
    124     * biomes.
    125     * EXA: ExtraBiomesXL
    126     *
    127     * Specifically, if your block is used BEFORE the Chunk is created, and placed in the terrain byte array directly.
    128     * If you add a new biome and you set the top/filler block, they need to be <256, nothing else.
    129     *
    130     * If you're adding a new ore, DON'T call this function.
    131     *
    132     * Normal mods such as '50 new ores' do not need to be below 256 so should use the normal getBlock
    133     */
    134    public Property getTerrainBlock(String category, String key, int defaultID, String comment)
    135    {
    136        return getBlockInternal(category, key, defaultID, comment, 0, 256);
    137    }
    138
    139    private Property getBlockInternal(String category, String key, int defaultID, String comment, int lower, int upper)
    140    {
    141        Property prop = get(category, key, -1, comment);
    142
    143        if (prop.getInt() != -1)
    144        {
    145            configMarkers[prop.getInt()] = true;
    146            return prop;
    147        }
    148        else
    149        {
    150            if (defaultID < lower)
    151            {
    152                FMLLog.warning(
    153                    "Mod attempted to get a block ID with a default in the Terrain Generation section, " +
    154                    "mod authors should make sure there defaults are above 256 unless explicitly needed " +
    155                    "for terrain generation. Most ores do not need to be below 256.");
    156                FMLLog.warning("Config \"%s\" Category: \"%s\" Key: \"%s\" Default: %d", fileName, category, key, defaultID);
    157                defaultID = upper - 1;
    158            }
    159
    160            if (Block.blocksList[defaultID] == null && !configMarkers[defaultID])
    161            {
    162                prop.set(defaultID);
    163                configMarkers[defaultID] = true;
    164                return prop;
    165            }
    166            else
    167            {
    168                for (int j = upper - 1; j > 0; j--)
    169                {
    170                    if (Block.blocksList[j] == null && !configMarkers[j])
    171                    {
    172                        prop.set(j);
    173                        configMarkers[j] = true;
    174                        return prop;
    175                    }
    176                }
    177
    178                throw new RuntimeException("No more block ids available for " + key);
    179            }
    180        }
    181    }
    182
    183    public Property getItem(String key, int defaultID) { return getItem(CATEGORY_ITEM, key, defaultID, null); }
    184    public Property getItem(String key, int defaultID, String comment) { return getItem(CATEGORY_ITEM, key, defaultID, comment); }
    185    public Property getItem(String category, String key, int defaultID) { return getItem(category, key, defaultID, null); }
    186
    187    public Property getItem(String category, String key, int defaultID, String comment)
    188    {
    189        Property prop = get(category, key, -1, comment);
    190        int defaultShift = defaultID + ITEM_SHIFT;
    191
    192        if (prop.getInt() != -1)
    193        {
    194            configMarkers[prop.getInt() + ITEM_SHIFT] = true;
    195            return prop;
    196        }
    197        else
    198        {
    199            if (defaultID < MAX_BLOCKS - ITEM_SHIFT)
    200            {
    201                FMLLog.warning(
    202                    "Mod attempted to get a item ID with a default value in the block ID section, " +
    203                    "mod authors should make sure there defaults are above %d unless explicitly needed " +
    204                    "so that all block ids are free to store blocks.", MAX_BLOCKS - ITEM_SHIFT);
    205                FMLLog.warning("Config \"%s\" Category: \"%s\" Key: \"%s\" Default: %d", fileName, category, key, defaultID);
    206            }
    207
    208            if (Item.itemsList[defaultShift] == null && !configMarkers[defaultShift] && defaultShift >= Block.blocksList.length)
    209            {
    210                prop.set(defaultID);
    211                configMarkers[defaultShift] = true;
    212                return prop;
    213            }
    214            else
    215            {
    216                for (int x = Item.itemsList.length - 1; x >= ITEM_SHIFT; x--)
    217                {
    218                    if (Item.itemsList[x] == null && !configMarkers[x])
    219                    {
    220                        prop.set(x - ITEM_SHIFT);
    221                        configMarkers[x] = true;
    222                        return prop;
    223                    }
    224                }
    225
    226                throw new RuntimeException("No more item ids available for " + key);
    227            }
    228        }
    229    }
    230
    231    public Property get(String category, String key, int defaultValue)
    232    {
    233        return get(category, key, defaultValue, null);
    234    }
    235
    236    public Property get(String category, String key, int defaultValue, String comment)
    237    {
    238        Property prop = get(category, key, Integer.toString(defaultValue), comment, INTEGER);
    239        if (!prop.isIntValue())
    240        {
    241            prop.set(defaultValue);
    242        }
    243        return prop;
    244    }
    245
    246    public Property get(String category, String key, boolean defaultValue)
    247    {
    248        return get(category, key, defaultValue, null);
    249    }
    250
    251    public Property get(String category, String key, boolean defaultValue, String comment)
    252    {
    253        Property prop = get(category, key, Boolean.toString(defaultValue), comment, BOOLEAN);
    254        if (!prop.isBooleanValue())
    255        {
    256            prop.set(defaultValue);
    257        }
    258        return prop;
    259    }
    260
    261    public Property get(String category, String key, double defaultValue)
    262    {
    263        return get(category, key, defaultValue, null);
    264    }
    265
    266    public Property get(String category, String key, double defaultValue, String comment)
    267    {
    268        Property prop = get(category, key, Double.toString(defaultValue), comment, DOUBLE);
    269        if (!prop.isDoubleValue())
    270        {
    271            prop.set(defaultValue);
    272        }
    273        return prop;
    274    }
    275
    276    public Property get(String category, String key, String defaultValue)
    277    {
    278        return get(category, key, defaultValue, null);
    279    }
    280
    281    public Property get(String category, String key, String defaultValue, String comment)
    282    {
    283        return get(category, key, defaultValue, comment, STRING);
    284    }
    285
    286    public Property get(String category, String key, String[] defaultValue)
    287    {
    288        return get(category, key, defaultValue, null);
    289    }
    290
    291    public Property get(String category, String key, String[] defaultValue, String comment)
    292    {
    293        return get(category, key, defaultValue, comment, STRING);
    294    }
    295
    296    public Property get(String category, String key, int[] defaultValue)
    297    {
    298        return get(category, key, defaultValue, null);
    299    }
    300
    301    public Property get(String category, String key, int[] defaultValue, String comment)
    302    {
    303        String[] values = new String[defaultValue.length];
    304        for (int i = 0; i < defaultValue.length; i++)
    305        {
    306            values[i] = Integer.toString(defaultValue[i]);
    307        }
    308
    309        Property prop =  get(category, key, values, comment, INTEGER);
    310        if (!prop.isIntList())
    311        {
    312            prop.set(values);
    313        }
    314
    315        return prop;
    316    }
    317
    318    public Property get(String category, String key, double[] defaultValue)
    319    {
    320        return get(category, key, defaultValue, null);
    321    }
    322
    323    public Property get(String category, String key, double[] defaultValue, String comment)
    324    {
    325        String[] values = new String[defaultValue.length];
    326        for (int i = 0; i < defaultValue.length; i++)
    327        {
    328            values[i] = Double.toString(defaultValue[i]);
    329        }
    330
    331        Property prop =  get(category, key, values, comment, DOUBLE);
    332
    333        if (!prop.isDoubleList())
    334        {
    335            prop.set(values);
    336        }
    337
    338        return prop;
    339    }
    340
    341    public Property get(String category, String key, boolean[] defaultValue)
    342    {
    343        return get(category, key, defaultValue, null);
    344    }
    345
    346    public Property get(String category, String key, boolean[] defaultValue, String comment)
    347    {
    348        String[] values = new String[defaultValue.length];
    349        for (int i = 0; i < defaultValue.length; i++)
    350        {
    351            values[i] = Boolean.toString(defaultValue[i]);
    352        }
    353
    354        Property prop =  get(category, key, values, comment, BOOLEAN);
    355
    356        if (!prop.isBooleanList())
    357        {
    358            prop.set(values);
    359        }
    360
    361        return prop;
    362    }
    363
    364    public Property get(String category, String key, String defaultValue, String comment, Property.Type type)
    365    {
    366        if (!caseSensitiveCustomCategories)
    367        {
    368            category = category.toLowerCase(Locale.ENGLISH);
    369        }
    370
    371        ConfigCategory cat = getCategory(category);
    372
    373        if (cat.containsKey(key))
    374        {
    375            Property prop = cat.get(key);
    376
    377            if (prop.getType() == null)
    378            {
    379                prop = new Property(prop.getName(), prop.getString(), type);
    380                cat.put(key, prop);
    381            }
    382
    383            prop.comment = comment;
    384            return prop;
    385        }
    386        else if (defaultValue != null)
    387        {
    388            Property prop = new Property(key, defaultValue, type);
    389            prop.set(defaultValue); //Set and mark as dirty to signify it should save
    390            cat.put(key, prop);
    391            prop.comment = comment;
    392            return prop;
    393        }
    394        else
    395        {
    396            return null;
    397        }
    398    }
    399
    400    public Property get(String category, String key, String[] defaultValue, String comment, Property.Type type)
    401    {
    402        if (!caseSensitiveCustomCategories)
    403        {
    404            category = category.toLowerCase(Locale.ENGLISH);
    405        }
    406
    407        ConfigCategory cat = getCategory(category);
    408
    409        if (cat.containsKey(key))
    410        {
    411            Property prop = cat.get(key);
    412
    413            if (prop.getType() == null)
    414            {
    415                prop = new Property(prop.getName(), prop.getString(), type);
    416                cat.put(key, prop);
    417            }
    418
    419            prop.comment = comment;
    420
    421            return prop;
    422        }
    423        else if (defaultValue != null)
    424        {
    425            Property prop = new Property(key, defaultValue, type);
    426            prop.comment = comment;
    427            cat.put(key, prop);
    428            return prop;
    429        }
    430        else
    431        {
    432            return null;
    433        }
    434    }
    435
    436    public boolean hasCategory(String category)
    437    {
    438        return categories.get(category) != null;
    439    }
    440
    441    public boolean hasKey(String category, String key)
    442    {
    443        ConfigCategory cat = categories.get(category);
    444        return cat != null && cat.containsKey(key);
    445    }
    446
    447    public void load()
    448    {
    449        if (PARENT != null && PARENT != this)
    450        {
    451            return;
    452        }
    453
    454        BufferedReader buffer = null;
    455        UnicodeInputStreamReader input = null;
    456        try
    457        {
    458            if (file.getParentFile() != null)
    459            {
    460                file.getParentFile().mkdirs();
    461            }
    462
    463            if (!file.exists() && !file.createNewFile())
    464            {
    465                return;
    466            }
    467
    468            if (file.canRead())
    469            {
    470                input = new UnicodeInputStreamReader(new FileInputStream(file), defaultEncoding);
    471                defaultEncoding = input.getEncoding();
    472                buffer = new BufferedReader(input);
    473
    474                String line;
    475                ConfigCategory currentCat = null;
    476                Property.Type type = null;
    477                ArrayList<String> tmpList = null;
    478                int lineNum = 0;
    479                String name = null;
    480
    481                while (true)
    482                {
    483                    lineNum++;
    484                    line = buffer.readLine();
    485
    486                    if (line == null)
    487                    {
    488                        break;
    489                    }
    490
    491                    Matcher start = CONFIG_START.matcher(line);
    492                    Matcher end = CONFIG_END.matcher(line);
    493
    494                    if (start.matches())
    495                    {
    496                        fileName = start.group(1);
    497                        categories = new TreeMap<String, ConfigCategory>();
    498                        continue;
    499                    }
    500                    else if (end.matches())
    501                    {
    502                        fileName = end.group(1);
    503                        Configuration child = new Configuration();
    504                        child.categories = categories;
    505                        this.children.put(fileName, child);
    506                        continue;
    507                    }
    508
    509                    int nameStart = -1, nameEnd = -1;
    510                    boolean skip = false;
    511                    boolean quoted = false;
    512
    513                    for (int i = 0; i < line.length() && !skip; ++i)
    514                    {
    515                        if (Character.isLetterOrDigit(line.charAt(i)) || ALLOWED_CHARS.indexOf(line.charAt(i)) != -1 || (quoted && line.charAt(i) != '"'))
    516                        {
    517                            if (nameStart == -1)
    518                            {
    519                                nameStart = i;
    520                            }
    521
    522                            nameEnd = i;
    523                        }
    524                        else if (Character.isWhitespace(line.charAt(i)))
    525                        {
    526                            // ignore space charaters
    527                        }
    528                        else
    529                        {
    530                            switch (line.charAt(i))
    531                            {
    532                                case '#':
    533                                    skip = true;
    534                                    continue;
    535
    536                                case '"':
    537                                    if (quoted)
    538                                    {
    539                                        quoted = false;
    540                                    }
    541                                    if (!quoted && nameStart == -1)
    542                                    {
    543                                        quoted = true;
    544                                    }
    545                                    break;
    546
    547                                case '{':
    548                                    name = line.substring(nameStart, nameEnd + 1);
    549                                    String qualifiedName = ConfigCategory.getQualifiedName(name, currentCat);
    550
    551                                    ConfigCategory cat = categories.get(qualifiedName);
    552                                    if (cat == null)
    553                                    {
    554                                        currentCat = new ConfigCategory(name, currentCat);
    555                                        categories.put(qualifiedName, currentCat);
    556                                    }
    557                                    else
    558                                    {
    559                                        currentCat = cat;
    560                                    }
    561                                    name = null;
    562
    563                                    break;
    564
    565                                case '}':
    566                                    if (currentCat == null)
    567                                    {
    568                                        throw new RuntimeException(String.format("Config file corrupt, attepted to close to many categories '%s:%d'", fileName, lineNum));
    569                                    }
    570                                    currentCat = currentCat.parent;
    571                                    break;
    572
    573                                case '=':
    574                                    name = line.substring(nameStart, nameEnd + 1);
    575
    576                                    if (currentCat == null)
    577                                    {
    578                                        throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
    579                                    }
    580
    581                                    Property prop = new Property(name, line.substring(i + 1), type, true);
    582                                    i = line.length();
    583
    584                                    currentCat.put(name, prop);
    585
    586                                    break;
    587
    588                                case ':':
    589                                    type = Property.Type.tryParse(line.substring(nameStart, nameEnd + 1).charAt(0));
    590                                    nameStart = nameEnd = -1;
    591                                    break;
    592
    593                                case '<':
    594                                    if (tmpList != null)
    595                                    {
    596                                        throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
    597                                    }
    598
    599                                    name = line.substring(nameStart, nameEnd + 1);
    600
    601                                    if (currentCat == null)
    602                                    {
    603                                        throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
    604                                    }
    605
    606                                    tmpList = new ArrayList<String>();
    607
    608                                    skip = true;
    609
    610                                    break;
    611
    612                                case '>':
    613                                    if (tmpList == null)
    614                                    {
    615                                        throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
    616                                    }
    617
    618                                    currentCat.put(name, new Property(name, tmpList.toArray(new String[tmpList.size()]), type));
    619                                    name = null;
    620                                    tmpList = null;
    621                                    type = null;
    622                                    break;
    623
    624                                default:
    625                                    throw new RuntimeException(String.format("Unknown character '%s' in '%s:%d'", line.charAt(i), fileName, lineNum));
    626                            }
    627                        }
    628                    }
    629
    630                    if (quoted)
    631                    {
    632                        throw new RuntimeException(String.format("Unmatched quote in '%s:%d'", fileName, lineNum));
    633                    }
    634                    else if (tmpList != null && !skip)
    635                    {
    636                        tmpList.add(line.trim());
    637                    }
    638                }
    639            }
    640        }
    641        catch (IOException e)
    642        {
    643            e.printStackTrace();
    644        }
    645        finally
    646        {
    647            if (buffer != null)
    648            {
    649                try
    650                {
    651                    buffer.close();
    652                } catch (IOException e){}
    653            }
    654            if (input != null)
    655            {
    656                try
    657                {
    658                    input.close();
    659                } catch (IOException e){}
    660            }
    661        }
    662
    663        resetChangedState();
    664    }
    665
    666    public void save()
    667    {
    668        if (PARENT != null && PARENT != this)
    669        {
    670            PARENT.save();
    671            return;
    672        }
    673
    674        try
    675        {
    676            if (file.getParentFile() != null)
    677            {
    678                file.getParentFile().mkdirs();
    679            }
    680
    681            if (!file.exists() && !file.createNewFile())
    682            {
    683                return;
    684            }
    685
    686            if (file.canWrite())
    687            {
    688                FileOutputStream fos = new FileOutputStream(file);
    689                BufferedWriter buffer = new BufferedWriter(new OutputStreamWriter(fos, defaultEncoding));
    690
    691                buffer.write("# Configuration file" + NEW_LINE + NEW_LINE);
    692
    693                if (children.isEmpty())
    694                {
    695                    save(buffer);
    696                }
    697                else
    698                {
    699                    for (Map.Entry<String, Configuration> entry : children.entrySet())
    700                    {
    701                        buffer.write("START: \"" + entry.getKey() + "\"" + NEW_LINE);
    702                        entry.getValue().save(buffer);
    703                        buffer.write("END: \"" + entry.getKey() + "\"" + NEW_LINE + NEW_LINE);
    704                    }
    705                }
    706
    707                buffer.close();
    708                fos.close();
    709            }
    710        }
    711        catch (IOException e)
    712        {
    713            e.printStackTrace();
    714        }
    715    }
    716
    717    private void save(BufferedWriter out) throws IOException
    718    {
    719        for (ConfigCategory cat : categories.values())
    720        {
    721            if (!cat.isChild())
    722            {
    723                cat.write(out, 0);
    724                out.newLine();
    725            }
    726        }
    727    }
    728
    729    public ConfigCategory getCategory(String category)
    730    {
    731        ConfigCategory ret = categories.get(category);
    732
    733        if (ret == null)
    734        {
    735            if (category.contains(CATEGORY_SPLITTER))
    736            {
    737                String[] hierarchy = category.split("\\"+CATEGORY_SPLITTER);
    738                ConfigCategory parent = categories.get(hierarchy[0]);
    739
    740                if (parent == null)
    741                {
    742                    parent = new ConfigCategory(hierarchy[0]);
    743                    categories.put(parent.getQualifiedName(), parent);
    744                    changed = true;
    745                }
    746
    747                for (int i = 1; i < hierarchy.length; i++)
    748                {
    749                    String name = ConfigCategory.getQualifiedName(hierarchy[i], parent);
    750                    ConfigCategory child = categories.get(name);
    751
    752                    if (child == null)
    753                    {
    754                        child = new ConfigCategory(hierarchy[i], parent);
    755                        categories.put(name, child);
    756                        changed = true;
    757                    }
    758
    759                    ret = child;
    760                    parent = child;
    761                }
    762            }
    763            else
    764            {
    765                ret = new ConfigCategory(category);
    766                categories.put(category, ret);
    767                changed = true;
    768            }
    769        }
    770
    771        return ret;
    772    }
    773
    774    public void removeCategory(ConfigCategory category)
    775    {
    776        for (ConfigCategory child : category.getChildren())
    777        {
    778            removeCategory(child);
    779        }
    780
    781        if (categories.containsKey(category.getQualifiedName()))
    782        {
    783            categories.remove(category.getQualifiedName());
    784            if (category.parent != null)
    785            {
    786                category.parent.removeChild(category);
    787            }
    788            changed = true;
    789        }
    790    }
    791
    792    public void addCustomCategoryComment(String category, String comment)
    793    {
    794        if (!caseSensitiveCustomCategories)
    795            category = category.toLowerCase(Locale.ENGLISH);
    796        getCategory(category).setComment(comment);
    797    }
    798
    799    private void setChild(String name, Configuration child)
    800    {
    801        if (!children.containsKey(name))
    802        {
    803            children.put(name, child);
    804            changed = true;
    805        }
    806        else
    807        {
    808            Configuration old = children.get(name);
    809            child.categories = old.categories;
    810            child.fileName = old.fileName;
    811            old.changed = true;
    812        }
    813    }
    814
    815    public static void enableGlobalConfig()
    816    {
    817        PARENT = new Configuration(new File(Loader.instance().getConfigDir(), "global.cfg"));
    818        PARENT.load();
    819    }
    820
    821    public static class UnicodeInputStreamReader extends Reader
    822    {
    823        private final InputStreamReader input;
    824        private final String defaultEnc;
    825
    826        public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException
    827        {
    828            defaultEnc = encoding;
    829            String enc = encoding;
    830            byte[] data = new byte[4];
    831
    832            PushbackInputStream pbStream = new PushbackInputStream(source, data.length);
    833            int read = pbStream.read(data, 0, data.length);
    834            int size = 0;
    835
    836            int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF);
    837            int bom24 = bom16 << 8 | (data[2] & 0xFF);
    838            int bom32 = bom24 << 8 | (data[3] & 0xFF);
    839
    840            if (bom24 == 0xEFBBBF)
    841            {
    842                enc = "UTF-8";
    843                size = 3;
    844            }
    845            else if (bom16 == 0xFEFF)
    846            {
    847                enc = "UTF-16BE";
    848                size = 2;
    849            }
    850            else if (bom16 == 0xFFFE)
    851            {
    852                enc = "UTF-16LE";
    853                size = 2;
    854            }
    855            else if (bom32 == 0x0000FEFF)
    856            {
    857                enc = "UTF-32BE";
    858                size = 4;
    859            }
    860            else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE,
    861            {                             //but if anyone ever runs across a 32LE file, i'd like to disect it.
    862                enc = "UTF-32LE";
    863                size = 4;
    864            }
    865
    866            if (size < read)
    867            {
    868                pbStream.unread(data, size, read - size);
    869            }
    870
    871            this.input = new InputStreamReader(pbStream, enc);
    872        }
    873
    874        public String getEncoding()
    875        {
    876            return input.getEncoding();
    877        }
    878
    879        @Override
    880        public int read(char[] cbuf, int off, int len) throws IOException
    881        {
    882            return input.read(cbuf, off, len);
    883        }
    884
    885        @Override
    886        public void close() throws IOException
    887        {
    888            input.close();
    889        }
    890    }
    891
    892    public boolean hasChanged()
    893    {
    894        if (changed) return true;
    895
    896        for (ConfigCategory cat : categories.values())
    897        {
    898            if (cat.hasChanged()) return true;
    899        }
    900
    901        for (Configuration child : children.values())
    902        {
    903            if (child.hasChanged()) return true;
    904        }
    905
    906        return false;
    907    }
    908
    909    private void resetChangedState()
    910    {
    911        changed = false;
    912        for (ConfigCategory cat : categories.values())
    913        {
    914            cat.resetChangedState();
    915        }
    916
    917        for (Configuration child : children.values())
    918        {
    919            child.resetChangedState();
    920        }
    921    }
    922
    923    public Set<String> getCategoryNames()
    924    {
    925        return ImmutableSet.copyOf(categories.keySet());
    926    }
    927}