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