412 lines
12 KiB
Java
412 lines
12 KiB
Java
package StevenDimDoors.mod_pocketDim.dungeon.pack;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStream;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.regex.Pattern;
|
|
|
|
import StevenDimDoors.mod_pocketDim.util.BaseConfigurationProcessor;
|
|
import StevenDimDoors.mod_pocketDim.util.ConfigurationProcessingException;
|
|
import StevenDimDoors.mod_pocketDim.util.WeightedContainer;
|
|
|
|
import com.google.common.base.CharMatcher;
|
|
import com.google.common.base.Splitter;
|
|
import com.google.common.primitives.Ints;
|
|
|
|
public class DungeonPackConfigReader extends BaseConfigurationProcessor<DungeonPackConfig>
|
|
{
|
|
private interface ILineProcessor
|
|
{
|
|
public void process(String line, DungeonPackConfig config) throws ConfigurationProcessingException;
|
|
}
|
|
|
|
//Note: These constants aren't static so that the memory will be released once
|
|
//we're done using it an instance. These aren't objects that we need to hold
|
|
//onto throughout the lifetime of MC, only at loading time.
|
|
|
|
private final int CONFIG_VERSION = 1;
|
|
private final int LOOKAHEAD_LIMIT = 1024;
|
|
private final int MAX_PRODUCT_WEIGHT = 10000;
|
|
private final int MIN_PRODUCT_WEIGHT = 1;
|
|
private final int DEFAULT_PRODUCT_WEIGHT = 100;
|
|
private final int MAX_DUNGEON_PACK_WEIGHT = 10000;
|
|
private final int MIN_DUNGEON_PACK_WEIGHT = 1;
|
|
private final int DEFAULT_DUNGEON_PACK_WEIGHT = 100;
|
|
private final int MAX_CONDITION_LENGTH = 20;
|
|
private final int MAX_PRODUCT_COUNT = MAX_CONDITION_LENGTH;
|
|
private final String COMMENT_MARKER = "##";
|
|
|
|
private final Pattern DUNGEON_TYPE_PATTERN = Pattern.compile("[A-Za-z0-9_\\-]{1,20}");
|
|
|
|
private final Splitter WHITESPACE_SPLITTER = Splitter.on(CharMatcher.WHITESPACE).omitEmptyStrings();
|
|
private final String SETTING_SEPARATOR = "=";
|
|
private final String RULE_SEPARATOR = "->";
|
|
private final String WEIGHT_SEPARATOR = "#";
|
|
|
|
public DungeonPackConfigReader() { }
|
|
|
|
@SuppressWarnings("resource")
|
|
@Override
|
|
public DungeonPackConfig readFromStream(InputStream inputStream) throws ConfigurationProcessingException
|
|
{
|
|
BufferedReader reader = null;
|
|
try
|
|
{
|
|
DungeonPackConfig config = new DungeonPackConfig();
|
|
reader = new BufferedReader(new InputStreamReader(inputStream));
|
|
|
|
//Check the config format version
|
|
int version = readVersion(reader);
|
|
if (version != CONFIG_VERSION)
|
|
{
|
|
throw new ConfigurationProcessingException("The dungeon pack config has an incompatible version.");
|
|
}
|
|
|
|
config.setTypeNames(new ArrayList<String>());
|
|
config.setRules(new ArrayList<DungeonChainRuleDefinition>());
|
|
|
|
//Read the dungeon types
|
|
if (findSection("Types", reader))
|
|
{
|
|
processLines(reader, config, new DungeonTypeProcessor());
|
|
}
|
|
|
|
//Load default settings
|
|
config.setAllowDuplicatesInChain(true);
|
|
config.setAllowPackChangeIn(true);
|
|
config.setAllowPackChangeOut(true);
|
|
config.setDistortDoorCoordinates(false);
|
|
config.setPackWeight(DEFAULT_DUNGEON_PACK_WEIGHT);
|
|
|
|
//Read the settings section
|
|
if (findSection("Settings", reader))
|
|
{
|
|
processLines(reader, config, new DungeonSettingsParser());
|
|
}
|
|
|
|
//Read the rules section
|
|
if (findSection("Rules", reader))
|
|
{
|
|
processLines(reader, config, new RuleDefinitionParser());
|
|
}
|
|
|
|
return config;
|
|
}
|
|
catch (ConfigurationProcessingException ex)
|
|
{
|
|
throw ex;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new ConfigurationProcessingException("An unexpected error occurred while trying to read the configuration file.", ex);
|
|
}
|
|
finally
|
|
{
|
|
if (reader != null)
|
|
{
|
|
try
|
|
{
|
|
reader.close();
|
|
}
|
|
catch (IOException ex) { }
|
|
}
|
|
}
|
|
}
|
|
|
|
private int readVersion(BufferedReader reader) throws ConfigurationProcessingException, IOException
|
|
{
|
|
String firstLine = reader.readLine();
|
|
String[] parts = firstLine.split("\\s", 0);
|
|
Integer version = null;
|
|
|
|
if (parts.length == 2 && parts[0].equalsIgnoreCase("version"))
|
|
{
|
|
version = Ints.tryParse(parts[1]);
|
|
}
|
|
|
|
if (version == null)
|
|
{
|
|
throw new ConfigurationProcessingException("Could not parse the config format version.");
|
|
}
|
|
return version;
|
|
}
|
|
|
|
private void processLines(BufferedReader reader, DungeonPackConfig config, ILineProcessor processor) throws IOException, ConfigurationProcessingException
|
|
{
|
|
String line;
|
|
|
|
while (reader.ready())
|
|
{
|
|
reader.mark(LOOKAHEAD_LIMIT);
|
|
line = reader.readLine();
|
|
if (!line.startsWith(COMMENT_MARKER))
|
|
{
|
|
line = line.trim();
|
|
if (line.length() > 0)
|
|
{
|
|
if (line.endsWith(":"))
|
|
{
|
|
//Consider this line a section header, reset the reader to undo consuming it
|
|
reader.reset();
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
processor.process(line, config);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean findSection(String name, BufferedReader reader) throws IOException, ConfigurationProcessingException
|
|
{
|
|
boolean found = false;
|
|
boolean matched = false;
|
|
String line = null;
|
|
String label = name + ":";
|
|
|
|
//Find the next section header
|
|
//Ignore blank lines and comment lines, stop for headers, and throw an exception for anything else
|
|
while (!found && reader.ready())
|
|
{
|
|
reader.mark(LOOKAHEAD_LIMIT);
|
|
line = reader.readLine();
|
|
if (!line.startsWith(COMMENT_MARKER))
|
|
{
|
|
line = line.trim();
|
|
if (line.length() > 0)
|
|
{
|
|
if (line.endsWith(":"))
|
|
{
|
|
//Consider this line a section header
|
|
found = true;
|
|
matched = line.equalsIgnoreCase(label);
|
|
}
|
|
else
|
|
{
|
|
//This line is invalid
|
|
throw new ConfigurationProcessingException("The dungeon pack config has an incorrect line where a section was expected: " + line);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Check if the header matches the one we're looking for.
|
|
//If it doesn't match, undo consuming the line so it can be read later.
|
|
if (found && !matched)
|
|
{
|
|
reader.reset();
|
|
}
|
|
return found;
|
|
}
|
|
|
|
private class DungeonTypeProcessor implements ILineProcessor
|
|
{
|
|
@Override
|
|
public void process(String line, DungeonPackConfig config) throws ConfigurationProcessingException
|
|
{
|
|
List<String> typeNames = config.getTypeNames();
|
|
|
|
//Check if the dungeon type has a name that meets our restrictions
|
|
if (DUNGEON_TYPE_PATTERN.matcher(line).matches())
|
|
{
|
|
//Ignore duplicate dungeon types
|
|
line = line.toUpperCase();
|
|
if (!typeNames.contains(line))
|
|
{
|
|
typeNames.add(line);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new ConfigurationProcessingException("The dungeon pack config has a dungeon type with illegal characters in its name: " + line);
|
|
}
|
|
}
|
|
}
|
|
|
|
private class DungeonSettingsParser implements ILineProcessor
|
|
{
|
|
@Override
|
|
public void process(String line, DungeonPackConfig config) throws ConfigurationProcessingException
|
|
{
|
|
//The various settings that we support will be hardcoded here.
|
|
//In the future, if we get more settings, then this should be
|
|
//refactored to use a more lookup-driven approach.
|
|
|
|
boolean valid = true;
|
|
String[] settingParts = line.split(SETTING_SEPARATOR, 2);
|
|
if (settingParts.length == 2)
|
|
{
|
|
try
|
|
{
|
|
String name = settingParts[0].trim();
|
|
String value = settingParts[1].trim();
|
|
if (name.equalsIgnoreCase("AllowDuplicatesInChain"))
|
|
{
|
|
config.setAllowDuplicatesInChain(parseBoolean(value));
|
|
}
|
|
else if (name.equalsIgnoreCase("AllowPackChangeOut"))
|
|
{
|
|
config.setAllowPackChangeOut(parseBoolean(value));
|
|
}
|
|
else if (name.equalsIgnoreCase("AllowPackChangeIn"))
|
|
{
|
|
config.setAllowPackChangeIn(parseBoolean(value));
|
|
}
|
|
else if (name.equalsIgnoreCase("DistortDoorCoordinates"))
|
|
{
|
|
config.setDistortDoorCoordinates(parseBoolean(value));
|
|
}
|
|
else if (name.equalsIgnoreCase("PackWeight"))
|
|
{
|
|
int weight = Integer.parseInt(value);
|
|
if (weight >= MIN_DUNGEON_PACK_WEIGHT && weight <= MAX_DUNGEON_PACK_WEIGHT)
|
|
{
|
|
config.setPackWeight(weight);
|
|
}
|
|
else
|
|
{
|
|
valid = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
valid = false;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
valid = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
valid = false;
|
|
}
|
|
|
|
if (!valid)
|
|
{
|
|
throw new ConfigurationProcessingException("The dungeon pack config has an invalid setting: " + line);
|
|
}
|
|
}
|
|
}
|
|
|
|
private class RuleDefinitionParser implements ILineProcessor
|
|
{
|
|
@Override
|
|
public void process(String definition, DungeonPackConfig config) throws ConfigurationProcessingException
|
|
{
|
|
String[] ruleParts;
|
|
String[] productParts;
|
|
String ruleCondition;
|
|
String ruleProduct;
|
|
ArrayList<String> condition;
|
|
ArrayList<WeightedContainer<String>> products;
|
|
List<String> typeNames = config.getTypeNames();
|
|
|
|
ruleParts = definition.toUpperCase().split(RULE_SEPARATOR, -1);
|
|
if (ruleParts.length != 2)
|
|
{
|
|
throw new ConfigurationProcessingException("The dungeon pack config has an invalid rule: " + definition);
|
|
}
|
|
|
|
ruleCondition = ruleParts[0];
|
|
ruleProduct = ruleParts[1];
|
|
condition = new ArrayList<String>();
|
|
products = new ArrayList<WeightedContainer<String>>();
|
|
|
|
for (String typeName : WHITESPACE_SPLITTER.split(ruleCondition))
|
|
{
|
|
if (isKnownDungeonType(typeName, typeNames))
|
|
{
|
|
condition.add(typeName);
|
|
}
|
|
else
|
|
{
|
|
throw new ConfigurationProcessingException("The dungeon pack config has a rule condition with an unknown dungeon type: " + typeName);
|
|
}
|
|
|
|
if (condition.size() > MAX_CONDITION_LENGTH)
|
|
{
|
|
throw new ConfigurationProcessingException("The dungeon pack config has a rule condition that is too long: " + definition);
|
|
}
|
|
}
|
|
|
|
for (String product : WHITESPACE_SPLITTER.split(ruleProduct))
|
|
{
|
|
Integer weight;
|
|
String typeName;
|
|
|
|
productParts = product.split(WEIGHT_SEPARATOR, -1);
|
|
if (productParts.length > 2 || productParts.length == 0)
|
|
{
|
|
throw new ConfigurationProcessingException("The dungeon pack config has a rule with an invalid product: " + product);
|
|
}
|
|
|
|
typeName = productParts[0];
|
|
if (isKnownDungeonType(typeName, typeNames))
|
|
{
|
|
if (productParts.length > 1)
|
|
{
|
|
weight = Ints.tryParse(productParts[1]);
|
|
if (weight == null || (weight > MAX_PRODUCT_WEIGHT) || (weight < MIN_PRODUCT_WEIGHT))
|
|
{
|
|
throw new ConfigurationProcessingException("The dungeon pack config has a rule with an invalid product weight: " + product);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
weight = DEFAULT_PRODUCT_WEIGHT;
|
|
}
|
|
products.add(new WeightedContainer<String>(typeName, weight));
|
|
}
|
|
else
|
|
{
|
|
throw new ConfigurationProcessingException("The dungeon pack config has an unknown dungeon type in a rule: " + typeName);
|
|
}
|
|
|
|
if (products.size() > MAX_PRODUCT_COUNT)
|
|
{
|
|
throw new ConfigurationProcessingException("The dungeon pack config has a rule with too many products: " + definition);
|
|
}
|
|
}
|
|
if (products.isEmpty())
|
|
{
|
|
throw new ConfigurationProcessingException("The dungeon pack config has a rule with no products: " + definition);
|
|
}
|
|
|
|
config.getRules().add( new DungeonChainRuleDefinition(condition, products) );
|
|
}
|
|
}
|
|
|
|
private static boolean isKnownDungeonType(String typeName, List<String> typeNames)
|
|
{
|
|
return typeName.equals(DungeonType.WILDCARD_TYPE.Name) || typeNames.contains(typeName);
|
|
}
|
|
|
|
private static boolean parseBoolean(String value)
|
|
{
|
|
if (value.equalsIgnoreCase("true"))
|
|
return true;
|
|
if (value.equalsIgnoreCase("false"))
|
|
return false;
|
|
throw new IllegalArgumentException("The boolean value must be either \"true\" or \"false\", ignoring case.");
|
|
}
|
|
|
|
@Override
|
|
public boolean canWrite()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void writeToStream(OutputStream outputStream, DungeonPackConfig data) throws ConfigurationProcessingException
|
|
{
|
|
throw new UnsupportedOperationException("DungeonPackConfigReader does not support writing.");
|
|
}
|
|
}
|