gt:             this.input = new InputStreamReader(pbStream, enc);
context: /**
   * This software is provided under the terms of the Minecraft Forge Public
   * License v1.0.
   */
  
  package net.minecraftforge.common.config;
  
  import static net.minecraftforge.common.config.Property.Type.BOOLEAN;
  import static net.minecraftforge.common.config.Property.Type.DOUBLE;
  import static net.minecraftforge.common.config.Property.Type.INTEGER;
  import static net.minecraftforge.common.config.Property.Type.STRING;
  
  import java.io.BufferedReader;
  import java.io.BufferedWriter;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.io.OutputStreamWriter;
  import java.io.PushbackInputStream;
  import java.io.Reader;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Set;
  import java.util.TreeMap;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import com.google.common.base.CharMatcher;
  import com.google.common.collect.ImmutableSet;
  
  import net.minecraftforge.fml.client.config.GuiConfig;
  import net.minecraftforge.fml.client.config.GuiConfigEntries;
  import net.minecraftforge.fml.client.config.GuiConfigEntries.IConfigEntry;
  import net.minecraftforge.fml.client.config.IConfigElement;
  import net.minecraftforge.fml.common.FMLLog;
  import net.minecraftforge.fml.common.Loader;
  import net.minecraftforge.fml.relauncher.FMLInjectionData;
  
  /**
   * This class offers advanced configurations capabilities, allowing to provide
   * various categories for configuration variables.
   */
  public class Configuration
  {
      public static final String CATEGORY_GENERAL = "general";
      public static final String CATEGORY_CLIENT = "client";
      public static final String ALLOWED_CHARS = "._-";
      public static final String DEFAULT_ENCODING = "UTF-8";
      public static final String CATEGORY_SPLITTER = ".";
      public static final String NEW_LINE;
      public static final String COMMENT_SEPARATOR = "##########################################################################################################";
      private static final String CONFIG_VERSION_MARKER = "~CONFIG_VERSION";
      private static final Pattern CONFIG_START = Pattern.compile("START: \"([^\\\"]+)\"");
      private static final Pattern CONFIG_END = Pattern.compile("END: \"([^\\\"]+)\"");
      public static final CharMatcher allowedProperties = CharMatcher.JAVA_LETTER_OR_DIGIT.or(CharMatcher.anyOf(ALLOWED_CHARS));
      private static Configuration PARENT = null;
  
      File file;
  
      private Map<String, ConfigCategory> categories = new TreeMap<String, ConfigCategory>();
      private Map<String, Configuration> children = new TreeMap<String, Configuration>();
  
      private boolean caseSensitiveCustomCategories;
      public String defaultEncoding = DEFAULT_ENCODING;
      private String fileName = null;
      public boolean isChild = false;
      private boolean changed = false;
      private String definedConfigVersion = null;
      private String loadedConfigVersion = null;
  
      static
      {
          NEW_LINE = System.getProperty("line.separator");
      }
  
      public Configuration(){}
  
      /**
       * Create a configuration file for the file given in parameter.
       */
      public Configuration(File file)
      {
          this(file, null);
      }
  
      /**
       * Create a configuration file for the file given in parameter with the provided config version number.
       */
      public Configuration(File file, String configVersion)
      {
          this.file = file;
          this.definedConfigVersion = configVersion;
          String basePath = ((File)(FMLInjectionData.data()[6])).getAbsolutePath().replace(File.separatorChar, '/').replace("/.", "");
          String path = file.getAbsolutePath().replace(File.separatorChar, '/').replace("/./", "/").replace(basePath, "");
          if (PARENT != null)
          {
              PARENT.setChild(path, this);
              isChild = true;
          }
          else
          {
              fileName = path;
              try
              {
                  load();
              }
              catch (Throwable e)
              {
                  File fileBak = new File(file.getAbsolutePath() + "_" +
                          new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".errored");
                  FMLLog.severe("An exception occurred while loading config file %s. This file will be renamed to %s " +
                          "and a new config file will be generated.", file.getName(), fileBak.getName());
                  e.printStackTrace();
  
                  file.renameTo(fileBak);
                  load();
              }
          }
      }
  
      public Configuration(File file, String configVersion, boolean caseSensitiveCustomCategories)
      {
          this(file, configVersion);
          this.caseSensitiveCustomCategories = caseSensitiveCustomCategories;
      }
  
      public Configuration(File file, boolean caseSensitiveCustomCategories)
      {
          this(file, null, caseSensitiveCustomCategories);
      }
  
      @Override
      public String toString()
      {
          return file.getAbsolutePath();
      }
  
      public String getDefinedConfigVersion()
      {
          return this.definedConfigVersion;
      }
  
      public String getLoadedConfigVersion()
      {
          return this.loadedConfigVersion;
      }
  
      /******************************************************************************************************************
       *
       * BOOLEAN gets
       *
       *****************************************************************************************************************/
  
      /**
       * Gets a boolean Property object without a comment using the default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @return a boolean Property object without a comment
       */
      public Property get(String category, String key, boolean defaultValue)
      {
          return get(category, key, defaultValue, null);
      }
  
      /**
       * Gets a boolean Property object with a comment using the default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @param comment a String comment
       * @return a boolean Property object without a comment
       */
      public Property get(String category, String key, boolean defaultValue, String comment)
      {
          Property prop = get(category, key, Boolean.toString(defaultValue), comment, BOOLEAN);
          prop.setDefaultValue(Boolean.toString(defaultValue));
  
          if (!prop.isBooleanValue())
          {
              prop.setValue(defaultValue);
          }
          return prop;
  
      }
  
      /**
       * Gets a boolean array Property without a comment using the default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @return a boolean array Property without a comment using these defaults: isListLengthFixed = false, maxListLength = -1
       */
      public Property get(String category, String key, boolean[] defaultValues)
      {
          return get(category, key, defaultValues, null);
      }
  
      /**
       * Gets a boolean array Property with a comment using the default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @return a boolean array Property with a comment using these defaults: isListLengthFixed = false, maxListLength = -1
       */
      public Property get(String category, String key, boolean[] defaultValues, String comment)
      {
          return get(category, key, defaultValues, comment, false, -1);
      }
  
      /**
       * Gets a boolean array Property with all settings defined.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @param isListLengthFixed boolean for whether this array is required to be a specific length (defined by the default value array
       *            length or maxListLength)
       * @param maxListLength the maximum length of this array, use -1 for no max length
       * @return a boolean array Property with all settings defined
       */
      public Property get(String category, String key, boolean[] defaultValues, String comment,
              boolean isListLengthFixed, int maxListLength)
      {
          String[] values = new String[defaultValues.length];
          for (int i = 0; i < defaultValues.length; i++)
          {
              values[i] = Boolean.toString(defaultValues[i]);
          }
  
          Property prop = get(category, key, values, comment, BOOLEAN);
          prop.setDefaultValues(values);
          prop.setIsListLengthFixed(isListLengthFixed);
          prop.setMaxListLength(maxListLength);
  
          if (!prop.isBooleanList())
          {
              prop.setValues(values);
          }
  
          return prop;
      }
  
      /* ****************************************************************************************************************
       *
       * INTEGER gets
       *
       *****************************************************************************************************************/
  
      /**
       * Gets an integer Property object without a comment using default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @return an integer Property object with default bounds of Integer.MIN_VALUE and Integer.MAX_VALUE
       */
      public Property get(String category, String key, int defaultValue)
      {
          return get(category, key, defaultValue, null, Integer.MIN_VALUE, Integer.MAX_VALUE);
      }
  
      /**
       * Gets an integer Property object with a comment using default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @param comment a String comment
       * @return an integer Property object with default bounds of Integer.MIN_VALUE and Integer.MAX_VALUE
       */
      public Property get(String category, String key, int defaultValue, String comment)
      {
          return get(category, key, defaultValue, comment, Integer.MIN_VALUE, Integer.MAX_VALUE);
      }
  
      /**
       * Gets an integer Property object with the defined comment, minimum and maximum bounds.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @param comment a String comment
       * @param minValue minimum boundary
       * @param maxValue maximum boundary
       * @return an integer Property object with the defined comment, minimum and maximum bounds
       */
      public Property get(String category, String key, int defaultValue, String comment, int minValue, int maxValue)
      {
          Property prop = get(category, key, Integer.toString(defaultValue), comment, INTEGER);
          prop.setDefaultValue(Integer.toString(defaultValue));
          prop.setMinValue(minValue);
          prop.setMaxValue(maxValue);
  
          if (!prop.isIntValue())
          {
              prop.setValue(defaultValue);
          }
          return prop;
      }
  
      /**
       * Gets an integer array Property object without a comment using default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @return an integer array Property object with default bounds of Integer.MIN_VALUE and Integer.MAX_VALUE, isListLengthFixed = false,
       *         maxListLength = -1
       */
      public Property get(String category, String key, int[] defaultValues)
      {
          return get(category, key, defaultValues, null);
      }
  
      /**
       * Gets an integer array Property object with a comment using default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @return an integer array Property object with default bounds of Integer.MIN_VALUE and Integer.MAX_VALUE, isListLengthFixed = false,
       *         maxListLength = -1
       */
      public Property get(String category, String key, int[] defaultValues, String comment)
      {
          return get(category, key, defaultValues, comment, Integer.MIN_VALUE, Integer.MAX_VALUE, false, -1);
      }
  
      /**
       * Gets an integer array Property object with the defined comment, minimum and maximum bounds.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @param minValue minimum boundary
       * @param maxValue maximum boundary
       * @return an integer array Property object with the defined comment, minimum and maximum bounds, isListLengthFixed
       *         = false, maxListLength = -1
       */
      public Property get(String category, String key, int[] defaultValues, String comment, int minValue, int maxValue)
      {
          return get(category, key, defaultValues, comment, minValue, maxValue, false, -1);
      }
  
      /**
       * Gets an integer array Property object with all settings defined.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @param minValue minimum boundary
       * @param maxValue maximum boundary
       * @param isListLengthFixed boolean for whether this array is required to be a specific length (defined by the default value array
       *            length or maxListLength)
       * @param maxListLength the maximum length of this array, use -1 for no max length
       * @return an integer array Property object with all settings defined
       */
      public Property get(String category, String key, int[] defaultValues, String comment, int minValue, int maxValue,
              boolean isListLengthFixed, int maxListLength)
      {
          String[] values = new String[defaultValues.length];
          for (int i = 0; i < defaultValues.length; i++)
          {
              values[i] = Integer.toString(defaultValues[i]);
          }
  
          Property prop = get(category, key, values, comment, INTEGER);
          prop.setDefaultValues(values);
          prop.setMinValue(minValue);
          prop.setMaxValue(maxValue);
          prop.setIsListLengthFixed(isListLengthFixed);
          prop.setMaxListLength(maxListLength);
  
          if (!prop.isIntList())
          {
              prop.setValues(values);
          }
  
          return prop;
      }
  
      /* ****************************************************************************************************************
       *
       * DOUBLE gets
       *
       *****************************************************************************************************************/
  
      /**
       * Gets a double Property object without a comment using default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @return a double Property object with default bounds of Double.MIN_VALUE and Double.MAX_VALUE
       */
      public Property get(String category, String key, double defaultValue)
      {
          return get(category, key, defaultValue, null);
      }
  
      /**
       * Gets a double Property object with a comment using default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @param comment a String comment
       * @return a double Property object with default bounds of Double.MIN_VALUE and Double.MAX_VALUE
       */
      public Property get(String category, String key, double defaultValue, String comment)
      {
          return get(category, key, defaultValue, comment, -Double.MAX_VALUE, Double.MAX_VALUE);
      }
  
      /**
       * Gets a double Property object with the defined comment, minimum and maximum bounds
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @param comment a String comment
       * @param minValue minimum boundary
       * @param maxValue maximum boundary
       * @return a double Property object with the defined comment, minimum and maximum bounds
       */
      public Property get(String category, String key, double defaultValue, String comment, double minValue, double maxValue)
      {
          Property prop = get(category, key, Double.toString(defaultValue), comment, DOUBLE);
          prop.setDefaultValue(Double.toString(defaultValue));
          prop.setMinValue(minValue);
          prop.setMaxValue(maxValue);
  
          if (!prop.isDoubleValue())
          {
              prop.setValue(defaultValue);
          }
          return prop;
      }
  
      /**
       * Gets a double array Property object without a comment using default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @return a double array Property object with default bounds of Double.MIN_VALUE and Double.MAX_VALUE, isListLengthFixed = false,
       *         maxListLength = -1
       */
      public Property get(String category, String key, double[] defaultValues)
      {
          return get(category, key, defaultValues, null);
      }
  
      /**
       * Gets a double array Property object without a comment using default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @return a double array Property object with default bounds of Double.MIN_VALUE and Double.MAX_VALUE, isListLengthFixed = false,
       *         maxListLength = -1
       */
      public Property get(String category, String key, double[] defaultValues, String comment)
      {
          return get(category, key, defaultValues, comment, -Double.MAX_VALUE, Double.MAX_VALUE, false, -1);
      }
  
      /**
       * Gets a double array Property object with the defined comment, minimum and maximum bounds.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @param minValue minimum boundary
       * @param maxValue maximum boundary
       * @return a double array Property object with the defined comment, minimum and maximum bounds, isListLengthFixed =
       *         false, maxListLength = -1
       */
      public Property get(String category, String key, double[] defaultValues, String comment, double minValue, double maxValue)
      {
          return get(category, key, defaultValues, comment, minValue, maxValue, false, -1);
      }
  
      /**
       * Gets a double array Property object with all settings defined.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @param minValue minimum boundary
       * @param maxValue maximum boundary
       * @param isListLengthFixed boolean for whether this array is required to be a specific length (defined by the default value array
       *            length or maxListLength)
       * @param maxListLength the maximum length of this array, use -1 for no max length
       * @return a double array Property object with all settings defined
       */
      public Property get(String category, String key, double[] defaultValues, String comment, double minValue, double maxValue,
              boolean isListLengthFixed, int maxListLength)
      {
          String[] values = new String[defaultValues.length];
          for (int i = 0; i < defaultValues.length; i++)
          {
              values[i] = Double.toString(defaultValues[i]);
          }
  
  
          Property prop = get(category, key, values, comment, DOUBLE);
          prop.setDefaultValues(values);
          prop.setMinValue(minValue);
          prop.setMaxValue(maxValue);
          prop.setIsListLengthFixed(isListLengthFixed);
          prop.setMaxListLength(maxListLength);
  
          if (!prop.isDoubleList())
          {
              prop.setValues(values);
          }
  
          return prop;
      }
  
      /* ****************************************************************************************************************
       *
       * STRING gets
       *
       *****************************************************************************************************************/
  
      /**
       * Gets a string Property without a comment using the default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @return a string Property with validationPattern = null, validValues = null
       */
      public Property get(String category, String key, String defaultValue)
      {
          return get(category, key, defaultValue, null);
      }
  
      /**
       * Gets a string Property with a comment using the default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @param comment a String comment
       * @return a string Property with validationPattern = null, validValues = null
       */
      public Property get(String category, String key, String defaultValue, String comment)
      {
          return get(category, key, defaultValue, comment, STRING);
      }
  
      /**
       * Gets a string Property with a comment using the defined validationPattern and otherwise default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @param comment a String comment
       * @param validationPattern a Pattern object for input validation
       * @return a string Property with the defined validationPattern, validValues = null
       */
      public Property get(String category, String key, String defaultValue, String comment, Pattern validationPattern)
      {
          Property prop = get(category, key, defaultValue, comment, STRING);
          prop.setValidationPattern(validationPattern);
          return prop;
      }
  
      /**
       * Gets a string Property with a comment using the defined validValues array and otherwise default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @param comment a String comment
       * @param validValues an array of valid values that this Property can be set to. If an array is provided the Config GUI control will be
       *            a value cycle button.
       * @return a string Property with the defined validValues array, validationPattern = null
       */
      public Property get(String category, String key, String defaultValue, String comment, String[] validValues)
      {
          Property prop = get(category, key, defaultValue, comment, STRING);
          prop.setValidValues(validValues);
          return prop;
      }
  
      /**
       * Gets a string array Property without a comment using the default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @return a string array Property with validationPattern = null, isListLengthFixed = false, maxListLength = -1
       */
      public Property get(String category, String key, String[] defaultValues)
      {
          return get(category, key, defaultValues, null, false, -1, null);
      }
  
      /**
       * Gets a string array Property with a comment using the default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @return a string array Property with validationPattern = null, isListLengthFixed = false, maxListLength = -1
       */
      public Property get(String category, String key, String[] defaultValues, String comment)
      {
          return get(category, key, defaultValues, comment, false, -1, null);
      }
  
      /**
       * Gets a string array Property with a comment using the defined validationPattern and otherwise default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @param validationPattern a Pattern object for input validation
       * @return a string array Property with the defined validationPattern, isListLengthFixed = false, maxListLength = -1
       */
      public Property get(String category, String key, String[] defaultValues, String comment, Pattern validationPattern)
      {
          return get(category, key, defaultValues, comment, false, -1, validationPattern);
      }
  
      /**
       * Gets a string array Property with a comment with all settings defined.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @param isListLengthFixed boolean for whether this array is required to be a specific length (defined by the default value array
       *            length or maxListLength)
       * @param maxListLength the maximum length of this array, use -1 for no max length
       * @param validationPattern a Pattern object for input validation
       * @return a string array Property with a comment with all settings defined
       */
      public Property get(String category, String key, String[] defaultValues, String comment,
              boolean isListLengthFixed, int maxListLength, Pattern validationPattern)
      {
          Property prop = get(category, key, defaultValues, comment, STRING);
          prop.setIsListLengthFixed(isListLengthFixed);
          prop.setMaxListLength(maxListLength);
          prop.setValidationPattern(validationPattern);
          return prop;
      }
  
      /* ****************************************************************************************************************
       *
       * GENERIC gets
       *
       *****************************************************************************************************************/
  
      /**
       * Gets a Property object of the specified type using default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValue the default value
       * @param comment a String comment
       * @param type a Property.Type enum value
       * @return a Property object of the specified type using default settings
       */
      public Property get(String category, String key, String defaultValue, String comment, Property.Type type)
      {
          ConfigCategory cat = getCategory(category);
  
          if (cat.containsKey(key))
          {
              Property prop = cat.get(key);
  
              if (prop.getType() == null)
              {
                  prop = new Property(prop.getName(), prop.getString(), type);
                  cat.put(key, prop);
              }
  
              prop.setDefaultValue(defaultValue);
              prop.setComment(comment);
              return prop;
          }
          else if (defaultValue != null)
          {
              Property prop = new Property(key, defaultValue, type);
              prop.setValue(defaultValue); //Set and mark as dirty to signify it should save
              cat.put(key, prop);
              prop.setDefaultValue(defaultValue);
              prop.setComment(comment);
              return prop;
          }
          else
          {
              return null;
          }
      }
  
      /**
       * Gets a list (array) Property object of the specified type using default settings.
       *
       * @param category the config category
       * @param key the Property key value
       * @param defaultValues an array containing the default values
       * @param comment a String comment
       * @param type a Property.Type enum value
       * @return a list (array) Property object of the specified type using default settings
       */
      public Property get(String category, String key, String[] defaultValues, String comment, Property.Type type)
      {
          ConfigCategory cat = getCategory(category);
  
          if (cat.containsKey(key))
          {
              Property prop = cat.get(key);
  
              if (prop.getType() == null)
              {
                  prop = new Property(prop.getName(), prop.getString(), type);
                  cat.put(key, prop);
              }
  
              prop.setDefaultValues(defaultValues);
              prop.setComment(comment);
  
              return prop;
          }
          else if (defaultValues != null)
          {
              Property prop = new Property(key, defaultValues, type);
              prop.setDefaultValues(defaultValues);
              prop.setComment(comment);
              cat.put(key, prop);
              return prop;
          }
          else
          {
              return null;
          }
      }
  
      /* ****************************************************************************************************************
       *
       * Other methods
       *
       *************************************************************************************************************** */
  
      public boolean hasCategory(String category)
      {
          if (!caseSensitiveCustomCategories)
              category = category.toLowerCase(Locale.ENGLISH);
          return categories.get(category) != null;
      }
  
      public boolean hasKey(String category, String key)
      {
          if (!caseSensitiveCustomCategories)
              category = category.toLowerCase(Locale.ENGLISH);
          ConfigCategory cat = categories.get(category);
          return cat != null && cat.containsKey(key);
      }
  
      public void load()
      {
          if (PARENT != null && PARENT != this)
          {
              return;
          }
  
          BufferedReader buffer = null;
          UnicodeInputStreamReader input = null;
          try
          {
              if (file.getParentFile() != null)
              {
                  file.getParentFile().mkdirs();
              }
  
              if (!file.exists())
              {
                  // Either a previous load attempt failed or the file is new; clear maps
                  categories.clear();
                  children.clear();
                  if (!file.createNewFile())
                      return;
              }
  
              if (file.canRead())
              {
                  input = new UnicodeInputStreamReader(new FileInputStream(file), defaultEncoding);
                  defaultEncoding = input.getEncoding();
                  buffer = new BufferedReader(input);
  
                  String line;
                  ConfigCategory currentCat = null;
                  Property.Type type = null;
                  ArrayList<String> tmpList = null;
                  int lineNum = 0;
                  String name = null;
                  loadedConfigVersion = null;
  
                  while (true)
                  {
                      lineNum++;
                      line = buffer.readLine();
  
                      if (line == null)
                      {
                          if (lineNum == 1)
                              loadedConfigVersion = definedConfigVersion;
                          break;
                      }
  
                      Matcher start = CONFIG_START.matcher(line);
                      Matcher end = CONFIG_END.matcher(line);
  
                      if (start.matches())
                      {
                          fileName = start.group(1);
                          categories = new TreeMap<String, ConfigCategory>();
                          continue;
                      }
                      else if (end.matches())
                      {
                          fileName = end.group(1);
                          Configuration child = new Configuration();
                          child.categories = categories;
                          this.children.put(fileName, child);
                          continue;
                      }
  
                      int nameStart = -1, nameEnd = -1;
                      boolean skip = false;
                      boolean quoted = false;
                      boolean isFirstNonWhitespaceCharOnLine = true;
  
                      for (int i = 0; i < line.length() && !skip; ++i)
                      {
                          if (Character.isLetterOrDigit(line.charAt(i)) || ALLOWED_CHARS.indexOf(line.charAt(i)) != -1 || (quoted && line.charAt(i) != '"'))
                          {
                              if (nameStart == -1)
                              {
                                  nameStart = i;
                              }
  
                              nameEnd = i;
                              isFirstNonWhitespaceCharOnLine = false;
                          }
                          else if (Character.isWhitespace(line.charAt(i)))
                          {
                              // ignore space characters
                          }
                          else
                          {
                              switch (line.charAt(i))
                              {
                                  case '#':
                                      if (tmpList != null) // allow special characters as part of string lists
                                          break;
                                      skip = true;
                                      continue;
  
                                  case '"':
                                      if (tmpList != null) // allow special characters as part of string lists
                                          break;
                                      if (quoted)
                                      {
                                          quoted = false;
                                      }
                                      if (!quoted && nameStart == -1)
                                      {
                                          quoted = true;
                                      }
                                      break;
  
                                  case '{':
                                      if (tmpList != null) // allow special characters as part of string lists
                                          break;
                                      name = line.substring(nameStart, nameEnd + 1);
                                      if (!caseSensitiveCustomCategories)
                                          name = name.toLowerCase(Locale.ENGLISH);
                                      String qualifiedName = ConfigCategory.getQualifiedName(name, currentCat);
  
                                      ConfigCategory cat = categories.get(qualifiedName);
                                      if (cat == null)
                                      {
                                          currentCat = new ConfigCategory(name, currentCat);
                                          categories.put(qualifiedName, currentCat);
                                      }
                                      else
                                      {
                                          currentCat = cat;
                                      }
                                      name = null;
  
                                      break;
  
                                  case '}':
                                      if (tmpList != null) // allow special characters as part of string lists
                                          break;
                                      if (currentCat == null)
                                      {
                                          throw new RuntimeException(String.format("Config file corrupt, attempted to close to many categories '%s:%d'", fileName, lineNum));
                                      }
                                      currentCat = currentCat.parent;
                                      break;
  
                                  case '=':
                                      if (tmpList != null) // allow special characters as part of string lists
                                          break;
                                      name = line.substring(nameStart, nameEnd + 1);
  
                                      if (currentCat == null)
                                      {
                                          throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
                                      }
  
                                      Property prop = new Property(name, line.substring(i + 1), type, true);
                                      i = line.length();
  
                                      currentCat.put(name, prop);
  
                                      break;
  
                                  case ':':
                                      if (tmpList != null) // allow special characters as part of string lists
                                          break;
                                      type = Property.Type.tryParse(line.substring(nameStart, nameEnd + 1).charAt(0));
                                      nameStart = nameEnd = -1;
                                      break;
  
                                  case '<':
                                      if ((tmpList != null && i + 1 == line.length()) || (tmpList == null && i + 1 != line.length()))
                                      {
                                          throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
                                      }
                                      else if (i + 1 == line.length())
                                      {
                                          name = line.substring(nameStart, nameEnd + 1);
  
                                          if (currentCat == null)
                                          {
                                              throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
                                          }
  
                                          tmpList = new ArrayList<String>();
  
                                          skip = true;
                                      }
  
                                      break;
  
                                  case '>':
                                      if (tmpList == null)
                                      {
                                          throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
                                      }
  
                                      if (isFirstNonWhitespaceCharOnLine)
                                      {
                                          currentCat.put(name, new Property(name, tmpList.toArray(new String[tmpList.size()]), type));
                                          name = null;
                                          tmpList = null;
                                          type = null;
                                      } // else allow special characters as part of string lists
                                      break;
  
                                  case '~':
                                      if (tmpList != null) // allow special characters as part of string lists
                                          break;
  
                                      if (line.startsWith(CONFIG_VERSION_MARKER))
                                      {
                                          int colon = line.indexOf(':');
                                          if (colon != -1)
                                              loadedConfigVersion = line.substring(colon + 1).trim();
  
                                          skip = true;
                                      }
                                      break;
  
                                  default:
                                      if (tmpList != null) // allow special characters as part of string lists
                                          break;
                                      throw new RuntimeException(String.format("Unknown character '%s' in '%s:%d'", line.charAt(i), fileName, lineNum));
                              }
                              isFirstNonWhitespaceCharOnLine = false;
                          }
                      }
  
                      if (quoted)
                      {
                          throw new RuntimeException(String.format("Unmatched quote in '%s:%d'", fileName, lineNum));
                      }
                      else if (tmpList != null && !skip)
                      {
                          tmpList.add(line.trim());
                      }
                  }
              }
          }
          catch (IOException e)
          {
              e.printStackTrace();
          }
          finally
          {
              if (buffer != null)
              {
                  try
                  {
                      buffer.close();
                  } catch (IOException e){}
              }
              if (input != null)
              {
                  try
                  {
                      input.close();
                  } catch (IOException e){}
              }
          }
  
          resetChangedState();
      }
  
      public void save()
      {
          if (PARENT != null && PARENT != this)
          {
              PARENT.save();
              return;
          }
  
          try
          {
              if (file.getParentFile() != null)
              {
                  file.getParentFile().mkdirs();
              }
  
              if (!file.exists() && !file.createNewFile())
              {
                  return;
              }
  
              if (file.canWrite())
              {
                  FileOutputStream fos = new FileOutputStream(file);
                  BufferedWriter buffer = new BufferedWriter(new OutputStreamWriter(fos, defaultEncoding));
  
                  buffer.write("# Configuration file" + NEW_LINE + NEW_LINE);
  
                  if (this.definedConfigVersion != null)
                      buffer.write(CONFIG_VERSION_MARKER + ": " + this.definedConfigVersion + NEW_LINE + NEW_LINE);
  
                  if (children.isEmpty())
                  {
                      save(buffer);
                  }
                  else
                  {
                      for (Map.Entry<String, Configuration> entry : children.entrySet())
                      {
                          buffer.write("START: \"" + entry.getKey() + "\"" + NEW_LINE);
                          entry.getValue().save(buffer);
                          buffer.write("END: \"" + entry.getKey() + "\"" + NEW_LINE + NEW_LINE);
                      }
                  }
  
                  buffer.close();
                  fos.close();
              }
          }
          catch (IOException e)
          {
              e.printStackTrace();
          }
      }
  
      private void save(BufferedWriter out) throws IOException
      {
          for (ConfigCategory cat : categories.values())
          {
              if (!cat.isChild())
              {
                  cat.write(out, 0);
                  out.newLine();
              }
          }
      }
  
      public ConfigCategory getCategory(String category)
      {
          if (!caseSensitiveCustomCategories)
              category = category.toLowerCase(Locale.ENGLISH);
  
          ConfigCategory ret = categories.get(category);
  
          if (ret == null)
          {
              if (category.contains(CATEGORY_SPLITTER))
              {
                  String[] hierarchy = category.split("\\"+CATEGORY_SPLITTER);
                  ConfigCategory parent = categories.get(hierarchy[0]);
  
                  if (parent == null)
                  {
                      parent = new ConfigCategory(hierarchy[0]);
                      categories.put(parent.getQualifiedName(), parent);
                      changed = true;
                  }
  
                  for (int i = 1; i < hierarchy.length; i++)
                  {
                      String name = ConfigCategory.getQualifiedName(hierarchy[i], parent);
                      ConfigCategory child = categories.get(name);
  
                      if (child == null)
                      {
                          child = new ConfigCategory(hierarchy[i], parent);
                          categories.put(name, child);
                          changed = true;
                      }
  
                      ret = child;
                      parent = child;
                  }
              }
              else
              {
                  ret = new ConfigCategory(category);
                  categories.put(category, ret);
                  changed = true;
              }
          }
  
          return ret;
      }
  
      public void removeCategory(ConfigCategory category)
      {
          for (ConfigCategory child : category.getChildren())
          {
              removeCategory(child);
          }
  
          if (categories.containsKey(category.getQualifiedName()))
          {
              categories.remove(category.getQualifiedName());
              if (category.parent != null)
              {
                  category.parent.removeChild(category);
              }
              changed = true;
          }
      }
  
      /**
       * Adds a comment to the specified ConfigCategory object
       *
       * @param category the config category
       * @param comment a String comment
       */
      public Configuration setCategoryComment(String category, String comment)
      {
          getCategory(category).setComment(comment);
          return this;
      }
  
      public void addCustomCategoryComment(String category, String comment)
      {
          this.setCategoryComment(category, comment);
      }
  
      /**
       * Adds a language key to the specified ConfigCategory object
       *
       * @param category the config category
       * @param langKey a language key string such as configcategory.general
       */
      public Configuration setCategoryLanguageKey(String category, String langKey)
      {
          getCategory(category).setLanguageKey(langKey);
          return this;
      }
  
      /**
       * Sets the custom IConfigEntry class that should be used in place of the standard entry class (which is just a button that
       * navigates into the category). This class MUST provide a constructor with the following parameter types: {@link GuiConfig} (the parent
       * GuiConfig screen will be provided), {@link GuiConfigEntries} (the parent GuiConfigEntries will be provided), {@link IConfigElement}
       * (the IConfigElement for this Property will be provided).
       *
       * @see GuiConfigEntries.ListEntryBase
       * @see GuiConfigEntries.StringEntry
       * @see GuiConfigEntries.BooleanEntry
       * @see GuiConfigEntries.DoubleEntry
       * @see GuiConfigEntries.IntegerEntry
       */
      public Configuration setCategoryConfigEntryClass(String category, Class<? extends IConfigEntry> clazz)
      {
          getCategory(category).setConfigEntryClass(clazz);
          return this;
      }
  
      /**
       * Sets the flag for whether or not this category can be edited while a world is running. Care should be taken to ensure
       * that only properties that are truly dynamic can be changed from the in-game options menu. Only set this flag to
       * true if all child properties/categories are unable to be modified while a world is running.
       */
      public Configuration setCategoryRequiresWorldRestart(String category, boolean requiresWorldRestart)
      {
          getCategory(category).setRequiresWorldRestart(requiresWorldRestart);
          return this;
      }
  
      /**
       * Sets whether or not this ConfigCategory requires Minecraft to be restarted when changed.
       * Defaults to false. Only set this flag to true if ALL child properties/categories require
       * Minecraft to be restarted when changed. Setting this flag will also prevent modification
       * of the child properties/categories while a world is running.
       */
      public Configuration setCategoryRequiresMcRestart(String category, boolean requiresMcRestart)
      {
          getCategory(category).setRequiresMcRestart(requiresMcRestart);
          return this;
      }
  
      /**
       * Sets the order that direct child properties of this config category will be written to the config file and will be displayed in
       * config GUIs.
       */
      public Configuration setCategoryPropertyOrder(String category, List<String> propOrder)
      {
          getCategory(category).setPropertyOrder(propOrder);
          return this;
      }
  
      private void setChild(String name, Configuration child)
      {
          if (!children.containsKey(name))
          {
              children.put(name, child);
              changed = true;
          }
          else
          {
              Configuration old = children.get(name);
              child.categories = old.categories;
              child.fileName = old.fileName;
              old.changed = true;
          }
      }
  
      public static void enableGlobalConfig()
      {
          PARENT = new Configuration(new File(Loader.instance().getConfigDir(), "global.cfg"));
          PARENT.load();
      }
  
      public static class UnicodeInputStreamReader extends Reader
      {
          private final InputStreamReader input;
          @SuppressWarnings("unused")
          private final String defaultEnc;
  
          public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException
          {
              defaultEnc = encoding;
              String enc = encoding;
              byte[] data = new byte[4];
  
              PushbackInputStream pbStream = new PushbackInputStream(source, data.length);
              int read = pbStream.read(data, 0, data.length);
              int size = 0;
  
              int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF);
              int bom24 = bom16 << 8 | (data[2] & 0xFF);
              int bom32 = bom24 << 8 | (data[3] & 0xFF);
  
              if (bom24 == 0xEFBBBF)
              {
                  enc = "UTF-8";
                  size = 3;
              }
              else if (bom16 == 0xFEFF)
              {
                  enc = "UTF-16BE";
                  size = 2;
              }
              else if (bom16 == 0xFFFE)
              {
                  enc = "UTF-16LE";
                  size = 2;
              }
              else if (bom32 == 0x0000FEFF)
              {
                  enc = "UTF-32BE";
                  size = 4;
              }
              else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE,
              {                             //but if anyone ever runs across a 32LE file, i'd like to dissect it.
                  enc = "UTF-32LE";
                  size = 4;
              }
  
              if (size < read)
              {
                  pbStream.unread(data, size, read - size);
              }
  
  