Progress on Implementing Dungeon Packs

Added code for parsing dungeon pack config files. The settings for our
built-in dungeons are now read from a file instead of being hardcoded.
One or two settings aren't being accessed yet and we still don't search
for other dungeon packs in the custom dungeon folder.  That'll come in
another commit.
This commit is contained in:
SenseiKiwi 2013-08-20 18:54:30 -04:00
parent dc7289a048
commit acab06115a
4 changed files with 452 additions and 52 deletions

View file

@ -7,6 +7,10 @@ public class DungeonPackConfig
private String name;
private ArrayList<String> typeNames;
private boolean allowDuplicatesInChain;
private boolean allowPackChangeIn;
private boolean allowPackChangeOut;
private boolean distortDoorCoordinates;
private int packWeight;
private ArrayList<DungeonChainRuleDefinition> rules;
public DungeonPackConfig() { }
@ -17,6 +21,10 @@ public class DungeonPackConfig
this.name = source.name;
this.typeNames = (ArrayList<String>) source.typeNames.clone();
this.allowDuplicatesInChain = source.allowDuplicatesInChain;
this.allowPackChangeIn = source.allowPackChangeIn;
this.allowPackChangeOut = source.allowPackChangeOut;
this.distortDoorCoordinates = source.distortDoorCoordinates;
this.packWeight = source.packWeight;
this.rules = (ArrayList<DungeonChainRuleDefinition>) source.rules.clone();
}
@ -75,4 +83,44 @@ public class DungeonPackConfig
{
return rules;
}
public boolean allowPackChangeIn()
{
return allowPackChangeIn;
}
public void setAllowPackChangeIn(boolean value)
{
this.allowPackChangeIn = value;
}
public boolean allowPackChangeOut()
{
return allowPackChangeOut;
}
public void setAllowPackChangeOut(boolean value)
{
this.allowPackChangeOut = value;
}
public int getPackWeight()
{
return packWeight;
}
public void setPackWeight(int packWeight)
{
this.packWeight = packWeight;
}
public boolean getDistortDoorCoordinates()
{
return distortDoorCoordinates;
}
public void setDistortDoorCoordinates(boolean value)
{
this.distortDoorCoordinates = value;
}
}

View file

@ -0,0 +1,386 @@
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 DEFAULT_PRODUCT_WEIGHT = 100;
private final int MAX_DUNGEON_PACK_WEIGHT = 10000;
private final int DEFAULT_DUNGEON_PACK_WEIGHT = 100;
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() { }
@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
{
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
{
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];
String value = settingParts[1];
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 >= 0 && weight <= MAX_DUNGEON_PACK_WEIGHT)
{
config.setPackWeight(weight);
}
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
{
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);
}
}
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 < 0))
{
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);
}
}
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.");
}
}

View file

@ -5,7 +5,6 @@ import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@ -22,12 +21,12 @@ import StevenDimDoors.mod_pocketDim.DungeonGenerator;
import StevenDimDoors.mod_pocketDim.LinkData;
import StevenDimDoors.mod_pocketDim.mod_pocketDim;
import StevenDimDoors.mod_pocketDim.dungeon.DungeonSchematic;
import StevenDimDoors.mod_pocketDim.dungeon.pack.DungeonChainRuleDefinition;
import StevenDimDoors.mod_pocketDim.dungeon.pack.DungeonPack;
import StevenDimDoors.mod_pocketDim.dungeon.pack.DungeonPackConfig;
import StevenDimDoors.mod_pocketDim.dungeon.pack.DungeonPackConfigReader;
import StevenDimDoors.mod_pocketDim.dungeon.pack.DungeonType;
import StevenDimDoors.mod_pocketDim.items.itemDimDoor;
import StevenDimDoors.mod_pocketDim.util.WeightedContainer;
import StevenDimDoors.mod_pocketDim.util.ConfigurationProcessingException;
public class DungeonHelper
{
@ -112,56 +111,21 @@ public class DungeonHelper
{
//This is a temporarily function for testing dungeon packs.
//It'll be removed later when we read dungeon configurations from files.
ArrayList<DungeonChainRuleDefinition> rules = new ArrayList<DungeonChainRuleDefinition>();
rules.add(parseDefinitionUnsafe("? ? ? ? ? ? ? ? -> Trap#20 SimpleHall#40 ComplexHall#10 Exit#20 DeadEnd#10"));
rules.add(parseDefinitionUnsafe("? ? ? ? -> Trap#18 SimpleHall#40 ComplexHall#10 Exit#18 DeadEnd#10 Hub#4"));
rules.add(parseDefinitionUnsafe("? ? ? -> ComplexHall Hub Trap SimpleHall Maze"));
rules.add(parseDefinitionUnsafe("? ? -> ComplexHall Hub Trap SimpleHall Maze"));
rules.add(parseDefinitionUnsafe("? -> ComplexHall#40 Hub#30 Trap#10 SimpleHall#10 Maze#10"));
rules.add(parseDefinitionUnsafe("-> ComplexHall#40 Hub#30 Trap#10 SimpleHall#10 Maze#10"));
String[] typeNames = "Hub Trap Maze Exit DeadEnd SimpleHall ComplexHall".toUpperCase().split(" ");
DungeonPackConfig config = new DungeonPackConfig();
config.setName("ruins");
config.setAllowDuplicatesInChain(false);
config.setRules(rules);
config.setTypeNames(new ArrayList<String>(Arrays.asList(typeNames)));
return config;
}
private static DungeonChainRuleDefinition parseDefinitionUnsafe(String definition)
{
//This is an improvised parsing function for rule definitions. Only for testing!!!
definition = definition.toUpperCase();
String[] parts = definition.split("->");
ArrayList<String> condition = new ArrayList<String>();
ArrayList<WeightedContainer<String>> products = new ArrayList<WeightedContainer<String>>();
for (String conditionPart : parts[0].split(" "))
DungeonPackConfig config;
try
{
if (!conditionPart.isEmpty())
condition.add(conditionPart);
config = (new DungeonPackConfigReader()).readFromResource("/schematics/ruins/rules.txt");
config.setName("ruins");
return config;
}
for (String product : parts[1].split(" "))
catch (ConfigurationProcessingException e)
{
if (!product.isEmpty())
{
String[] productParts = product.split("#");
String productType = productParts[0];
int weight = (productParts.length > 1) ? Integer.parseInt(productParts[1]) : 100;
products.add(new WeightedContainer<String>(productType, weight));
}
//FIXME TEMPORARY DEBUG PRINT, DO SOMETHING BETTER HERE
System.err.println("OH GOD SOMETHING WENT WRONG WITH THE DEFAULT DUNGEON PACK CONFIG");
e.printStackTrace();
return null;
}
return new DungeonChainRuleDefinition(condition, products);
}
public List<DungeonGenerator> getRegisteredDungeons()

View file

@ -9,10 +9,12 @@ DeadEnd
Maze
Settings:
AllowRepetitionsInBranch = false
AllowDuplicatesInChain = false
AllowPackChangeOut = true
AllowPackChangeIn = true
PackWeight = 100
DistortDoorCoordinates = true
## Prevent this pack from being selected for transitioning in once we've transitioned out
AllowPackChangeIn = false
Rules: