Java tutorial
/* * Minecraft Forge * Copyright (c) 2016. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation version 2.1 * of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /** * 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; import org.apache.commons.io.IOUtils; /** * 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. */ private void runConfiguration(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) { runConfiguration(file, configVersion); } public Configuration(File file, String configVersion, boolean caseSensitiveCustomCategories) { this.caseSensitiveCustomCategories = caseSensitiveCustomCategories; runConfiguration(file, configVersion); } 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 { IOUtils.closeQuietly(buffer); IOUtils.closeQuietly(input); } 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); } this.input = new InputStreamReader(pbStream, enc); } public String getEncoding() { return input.getEncoding(); } @Override public int read(char[] cbuf, int off, int len) throws IOException { return input.read(cbuf, off, len); } @Override public void close() throws IOException { input.close(); } } public boolean hasChanged() { if (changed) return true; for (ConfigCategory cat : categories.values()) { if (cat.hasChanged()) return true; } for (Configuration child : children.values()) { if (child.hasChanged()) return true; } return false; } private void resetChangedState() { changed = false; for (ConfigCategory cat : categories.values()) { cat.resetChangedState(); } for (Configuration child : children.values()) { child.resetChangedState(); } } public Set<String> getCategoryNames() { return ImmutableSet.copyOf(categories.keySet()); } /** * Renames a property in a given category. * * @param category the category in which the property resides * @param oldPropName the existing property name * @param newPropName the new property name * @return true if the category and property exist, false otherwise */ public boolean renameProperty(String category, String oldPropName, String newPropName) { if (hasCategory(category)) { if (getCategory(category).containsKey(oldPropName) && !oldPropName.equalsIgnoreCase(newPropName)) { get(category, newPropName, getCategory(category).get(oldPropName).getString(), ""); getCategory(category).remove(oldPropName); return true; } } return false; } /** * Moves a property from one category to another. * * @param oldCategory the category the property currently resides in * @param propName the name of the property to move * @param newCategory the category the property should be moved to * @return true if the old category and property exist, false otherwise */ public boolean moveProperty(String oldCategory, String propName, String newCategory) { if (!oldCategory.equals(newCategory)) if (hasCategory(oldCategory)) if (getCategory(oldCategory).containsKey(propName)) { getCategory(newCategory).put(propName, getCategory(oldCategory).remove(propName)); return true; } return false; } /** * Copies property objects from another Configuration object to this one using the list of category names. Properties that only exist in the * "from" object are ignored. Pass null for the ctgys array to include all categories. */ public void copyCategoryProps(Configuration fromConfig, String[] ctgys) { if (ctgys == null) ctgys = this.getCategoryNames().toArray(new String[this.getCategoryNames().size()]); for (String ctgy : ctgys) if (fromConfig.hasCategory(ctgy) && this.hasCategory(ctgy)) { ConfigCategory thiscc = this.getCategory(ctgy); ConfigCategory fromcc = fromConfig.getCategory(ctgy); for (Entry<String, Property> entry : thiscc.getValues().entrySet()) if (fromcc.containsKey(entry.getKey())) thiscc.put(entry.getKey(), fromcc.get(entry.getKey())); } } /** * Creates a string property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param comment A brief description what the property does. * @return The value of the new string property. */ public String getString(String name, String category, String defaultValue, String comment) { return getString(name, category, defaultValue, comment, name, null); } /** * Creates a string property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param comment A brief description what the property does. * @param langKey A language key used for localization of GUIs * @return The value of the new string property. */ public String getString(String name, String category, String defaultValue, String comment, String langKey) { return getString(name, category, defaultValue, comment, langKey, null); } /** * Creates a string property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param comment A brief description what the property does. * @return The value of the new string property. */ public String getString(String name, String category, String defaultValue, String comment, Pattern pattern) { return getString(name, category, defaultValue, comment, name, pattern); } /** * Creates a string property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param comment A brief description what the property does. * @param langKey A language key used for localization of GUIs * @return The value of the new string property. */ public String getString(String name, String category, String defaultValue, String comment, String langKey, Pattern pattern) { Property prop = this.get(category, name, defaultValue); prop.setLanguageKey(langKey); prop.setValidationPattern(pattern); prop.setComment(comment + " [default: " + defaultValue + "]"); return prop.getString(); } /** * Creates a string property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param comment A brief description what the property does. * @param validValues A list of valid values that this property can be set to. * @return The value of the new string property. */ public String getString(String name, String category, String defaultValue, String comment, String[] validValues) { return getString(name, category, defaultValue, comment, validValues, name); } /** * Creates a string property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param comment A brief description what the property does. * @param validValues A list of valid values that this property can be set to. * @param langKey A language key used for localization of GUIs * @return The value of the new string property. */ public String getString(String name, String category, String defaultValue, String comment, String[] validValues, String langKey) { Property prop = this.get(category, name, defaultValue); prop.setValidValues(validValues); prop.setLanguageKey(langKey); prop.setComment(comment + " [default: " + defaultValue + "]"); return prop.getString(); } /** * Creates a string list property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValues Default values of the property. * @param comment A brief description what the property does. * @return The value of the new string property. */ public String[] getStringList(String name, String category, String[] defaultValues, String comment) { return getStringList(name, category, defaultValues, comment, null, name); } /** * Creates a string list property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param comment A brief description what the property does. * @return The value of the new string property. */ public String[] getStringList(String name, String category, String[] defaultValue, String comment, String[] validValues) { return getStringList(name, category, defaultValue, comment, validValues, name); } /** * Creates a string list property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param comment A brief description what the property does. * @return The value of the new string property. */ public String[] getStringList(String name, String category, String[] defaultValue, String comment, String[] validValues, String langKey) { Property prop = this.get(category, name, defaultValue); prop.setLanguageKey(langKey); prop.setValidValues(validValues); prop.setComment(comment + " [default: " + prop.getDefault() + "]"); return prop.getStringList(); } /** * Creates a boolean property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param comment A brief description what the property does. * @return The value of the new boolean property. */ public boolean getBoolean(String name, String category, boolean defaultValue, String comment) { return getBoolean(name, category, defaultValue, comment, name); } /** * Creates a boolean property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param comment A brief description what the property does. * @param langKey A language key used for localization of GUIs * @return The value of the new boolean property. */ public boolean getBoolean(String name, String category, boolean defaultValue, String comment, String langKey) { Property prop = this.get(category, name, defaultValue); prop.setLanguageKey(langKey); prop.setComment(comment + " [default: " + defaultValue + "]"); return prop.getBoolean(defaultValue); } /** * Creates a integer property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param minValue Minimum value of the property. * @param maxValue Maximum value of the property. * @param comment A brief description what the property does. * @return The value of the new integer property. */ public int getInt(String name, String category, int defaultValue, int minValue, int maxValue, String comment) { return getInt(name, category, defaultValue, minValue, maxValue, comment, name); } /** * Creates a integer property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param minValue Minimum value of the property. * @param maxValue Maximum value of the property. * @param comment A brief description what the property does. * @param langKey A language key used for localization of GUIs * @return The value of the new integer property. */ public int getInt(String name, String category, int defaultValue, int minValue, int maxValue, String comment, String langKey) { Property prop = this.get(category, name, defaultValue); prop.setLanguageKey(langKey); prop.setComment(comment + " [range: " + minValue + " ~ " + maxValue + ", default: " + defaultValue + "]"); prop.setMinValue(minValue); prop.setMaxValue(maxValue); return prop.getInt(defaultValue) < minValue ? minValue : (prop.getInt(defaultValue) > maxValue ? maxValue : prop.getInt(defaultValue)); } /** * Creates a float property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param minValue Minimum value of the property. * @param maxValue Maximum value of the property. * @param comment A brief description what the property does. * @return The value of the new float property. */ public float getFloat(String name, String category, float defaultValue, float minValue, float maxValue, String comment) { return getFloat(name, category, defaultValue, minValue, maxValue, comment, name); } /** * Creates a float property. * * @param name Name of the property. * @param category Category of the property. * @param defaultValue Default value of the property. * @param minValue Minimum value of the property. * @param maxValue Maximum value of the property. * @param comment A brief description what the property does. * @param langKey A language key used for localization of GUIs * @return The value of the new float property. */ public float getFloat(String name, String category, float defaultValue, float minValue, float maxValue, String comment, String langKey) { Property prop = this.get(category, name, Float.toString(defaultValue), name); prop.setLanguageKey(langKey); prop.setComment(comment + " [range: " + minValue + " ~ " + maxValue + ", default: " + defaultValue + "]"); prop.setMinValue(minValue); prop.setMaxValue(maxValue); try { return Float.parseFloat(prop.getString()) < minValue ? minValue : (Float.parseFloat(prop.getString()) > maxValue ? maxValue : Float.parseFloat(prop.getString())); } catch (Exception e) { e.printStackTrace(); } return defaultValue; } public File getConfigFile() { return file; } }