diff --git a/StevenDimDoors/mod_pocketDim/DDProperties.java b/StevenDimDoors/mod_pocketDim/DDProperties.java index 243be95..a10cb31 100644 --- a/StevenDimDoors/mod_pocketDim/DDProperties.java +++ b/StevenDimDoors/mod_pocketDim/DDProperties.java @@ -214,9 +214,9 @@ public class DDProperties "Sets the chance (out of " + RiftGenerator.MAX_CLUSTER_GENERATION_CHANCE + ") that a cluster of rifts will " + "generate in a given chunk. The default chance is 3.").getInt(); - GatewayGenerationChance = config.get(Configuration.CATEGORY_GENERAL, "Gateway Generation Chance", 40, + GatewayGenerationChance = config.get(Configuration.CATEGORY_GENERAL, "Gateway Generation Chance", 10, "Sets the chance (out of " + RiftGenerator.MAX_GATEWAY_GENERATION_CHANCE + ") that a Rift Gateway will " + - "generate in a given chunk. The default chance is 40.").getInt(); + "generate in a given chunk. The default chance is 10.").getInt(); RiftSpreadModifier = config.get(Configuration.CATEGORY_GENERAL, "Rift Spread Modifier", 3, "Sets the number of times a rift can spread. 0 prevents rifts from spreading at all. " + diff --git a/StevenDimDoors/mod_pocketDim/DungeonGenerator.java b/StevenDimDoors/mod_pocketDim/DungeonGenerator.java index a0eb9f9..6c6ea16 100644 --- a/StevenDimDoors/mod_pocketDim/DungeonGenerator.java +++ b/StevenDimDoors/mod_pocketDim/DungeonGenerator.java @@ -1,31 +1,80 @@ package StevenDimDoors.mod_pocketDim; +import java.io.File; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.Random; +import StevenDimDoors.mod_pocketDim.dungeon.pack.DungeonType; +import StevenDimDoors.mod_pocketDim.helpers.DungeonHelper; + import net.minecraft.world.World; public class DungeonGenerator implements Serializable { + //This static field is hax so that I don't have to add an instance field to DungeonGenerator to support DungeonType. + //Otherwise it would have to be serializable and all sorts of problems would arise. + private static final HashMap dungeonTypes = new HashMap(); + public int weight; public String schematicPath; public ArrayList sideRifts = new ArrayList(); - public LinkData exitLink; - public static Random rand = new Random(); + public LinkData exitLink; public boolean isOpen; public int sideDoorsSoFar=0; public int exitDoorsSoFar=0; public int deadEndsSoFar=0; - - - public DungeonGenerator(int weight, String schematicPath, Boolean isOpen) + public DungeonGenerator(int weight, String schematicPath, boolean isOpen, DungeonType dungeonType) { - this.weight=weight; - this.schematicPath=schematicPath; - this.isOpen=isOpen; + this.weight = weight; + this.schematicPath = schematicPath; + this.isOpen = isOpen; + + dungeonTypes.put(this, dungeonType); //Hax... + } + + public DungeonType getDungeonType() + { + DungeonType type = dungeonTypes.get(this); + if (type == null) + { + //Infer the dungeon's type from its file name + //There is minimal risk of us applying this to untagged dungeons and this'll be phased out + //when we get the new save format. + try + { + File file = new File(schematicPath); + String typeName = file.getName().split("_")[0]; + type = DungeonHelper.instance().RuinsPack.getType(typeName); + } + catch (Exception e) { } + if (type == null) + { + type = DungeonType.UNKNOWN_TYPE; + } + dungeonTypes.put(this, type); + } + return type; + } + + @Override + public int hashCode() + { + return (schematicPath != null) ? schematicPath.hashCode() : 0; + } + + @Override + public boolean equals(Object other) + { + return equals((DungeonGenerator) other); + } + + public boolean equals(DungeonGenerator other) + { + return ((this.schematicPath != null && this.schematicPath.equals(other.schematicPath)) || + (this.schematicPath == other.schematicPath)); } } \ No newline at end of file diff --git a/StevenDimDoors/mod_pocketDim/EventHookContainer.java b/StevenDimDoors/mod_pocketDim/EventHookContainer.java index 95742f3..d2e12b1 100644 --- a/StevenDimDoors/mod_pocketDim/EventHookContainer.java +++ b/StevenDimDoors/mod_pocketDim/EventHookContainer.java @@ -46,32 +46,26 @@ public class EventHookContainer dimHelper.instance.interDimLinkList.clear(); dimHelper.instance.initPockets(); } - for (Integer ids : dimHelper.getIDs()) - { - World world = dimHelper.getWorld(ids); + + //TODO: In the future, we should iterate over DimHelper's dimension list. We ignore other dimensions anyway. + for (int dimensionID : dimHelper.getIDs()) + { + World world = dimHelper.getWorld(dimensionID); int linkCount = 0; - if (dimHelper.dimList.containsKey(world.provider.dimensionId)) + if (dimHelper.dimList.containsKey(dimensionID)) { - //TODO added temporary Try/catch block to prevent a crash here, getLinksInDim needs to be looked at - try - { - for (LinkData link:dimHelper.instance.getDimData(world.provider.dimensionId).getLinksInDim()) - { - if (!mod_pocketDim.blockRift.isBlockImmune(world, link.locXCoord, link.locYCoord, link.locZCoord)) - { - dimHelper.getWorld(link.locDimID).setBlock(link.locXCoord, link.locYCoord, link.locZCoord, properties.RiftBlockID); - } - linkCount++; - if (linkCount >= 100) - { - break; - } - } - } - catch(Exception e) + for (LinkData link : dimHelper.instance.getDimData(dimensionID).getLinksInDim()) { - e.printStackTrace(); + if (!mod_pocketDim.blockRift.isBlockImmune(world, link.locXCoord, link.locYCoord, link.locZCoord)) + { + world.setBlock(link.locXCoord, link.locYCoord, link.locZCoord, properties.RiftBlockID); + } + linkCount++; + if (linkCount >= 100) + { + break; + } } } } diff --git a/StevenDimDoors/mod_pocketDim/RiftGenerator.java b/StevenDimDoors/mod_pocketDim/RiftGenerator.java index 7b7f36e..1009b31 100644 --- a/StevenDimDoors/mod_pocketDim/RiftGenerator.java +++ b/StevenDimDoors/mod_pocketDim/RiftGenerator.java @@ -24,6 +24,9 @@ public class RiftGenerator implements IWorldGenerator private static final int CHUNK_LENGTH = 16; private static final int GATEWAY_RADIUS = 4; private static final int MAX_GATEWAY_GENERATION_ATTEMPTS = 10; + private static final int NETHER_CHANCE_CORRECTION = 4; + private static final int OVERWORLD_DIMENSION_ID = 0; + private static final int NETHER_DIMENSION_ID = -1; private static DDProperties properties = null; public RiftGenerator() @@ -43,16 +46,30 @@ public class RiftGenerator implements IWorldGenerator return; } //This check prevents a crash related to superflat worlds not loading World 0 - if (dimHelper.getWorld(0) == null) + if (dimHelper.getWorld(OVERWORLD_DIMENSION_ID) == null) { return; } int x, y, z; int attempts; - int blockID; + int correction; boolean valid; LinkData link; + + //Check if we're generating things in the Nether + if (world.provider.dimensionId == NETHER_DIMENSION_ID) + { + //The terrain in the Nether makes it much harder for our gateway spawning algorithm to find a spot to place a gateway. + //Tests show that only about 15% of attempts succeed. Compensate for this by multiplying the chance of generation + //by a correction factor. + correction = NETHER_CHANCE_CORRECTION; + } + else + { + //No correction + correction = 1; + } //Randomly decide whether to place a cluster of rifts here if (random.nextInt(MAX_CLUSTER_GENERATION_CHANCE) < properties.ClusterGenerationChance) @@ -91,7 +108,8 @@ public class RiftGenerator implements IWorldGenerator //Check if generating structures is enabled and randomly decide whether to place a Rift Gateway here. //This only happens if a rift cluster was NOT generated. - else if (random.nextInt(MAX_GATEWAY_GENERATION_CHANCE) < properties.GatewayGenerationChance && isStructureGenerationAllowed()) + else if (random.nextInt(MAX_GATEWAY_GENERATION_CHANCE) < properties.GatewayGenerationChance * correction && + isStructureGenerationAllowed()) { valid = false; x = y = z = 0; //Stop the compiler from freaking out @@ -117,59 +135,74 @@ public class RiftGenerator implements IWorldGenerator //If the current dimension isn't Limbo, build a Rift Gateway out of Stone Bricks if (world.provider.dimensionId != properties.LimboDimensionID) { - blockID = Block.stoneBrick.blockID; - - //Replace some of the ground around the gateway with bricks - for (int xc = -GATEWAY_RADIUS; xc <= GATEWAY_RADIUS; xc++) - { - for (int zc= -GATEWAY_RADIUS; zc <= GATEWAY_RADIUS; zc++) - { - //Check that the block is supported by an opaque block. - //This prevents us from building over a cliff, on the peak of a mountain, - //or the surface of the ocean or a frozen lake. - if (world.isBlockOpaqueCube(x + xc, y - 2, z + zc)) - { - //Randomly choose whether to place bricks or not. The math is designed so that the - //chances of placing a block decrease as we get farther from the gateway's center. - if (Math.abs(xc) + Math.abs(zc) < random.nextInt(2) + 3) - { - //Place Stone Bricks - world.setBlock(x + xc, y - 1, z + zc, blockID, 0, 3); - } - else if (Math.abs(xc) + Math.abs(zc) < random.nextInt(3) + 3) - { - //Place Cracked Stone Bricks - world.setBlock(x + xc, y - 1, z + zc, blockID, 2, 3); - } - } - } - } - - //Use Chiseled Stone Bricks to top off the pillars around the door - world.setBlock(x, y + 2, z + 1, blockID, 3, 3); - world.setBlock(x, y + 2, z - 1, blockID, 3, 3); + createStoneGateway(world, x, y, z, random); } else { - //Build the gateway out of Unraveled Fabric. Since nearly all the blocks in Limbo are of - //that type, there is no point replacing the ground. Just build the tops of the columns here. - blockID = properties.LimboBlockID; - world.setBlock(x, y + 2, z + 1, blockID, 0, 3); - world.setBlock(x, y + 2, z - 1, blockID, 0, 3); + createLimboGateway(world, x, y, z); } //Place the shiny transient door into a dungeon itemDimDoor.placeDoorBlock(world, x, y + 1, z, 0, mod_pocketDim.transientDoor); - - //Build the columns around the door - world.setBlock(x, y + 1, z - 1, blockID, 0, 3); - world.setBlock(x, y + 1, z + 1, blockID, 0, 3); - world.setBlock(x, y, z - 1, blockID, 0, 3); - world.setBlock(x, y, z + 1, blockID, 0, 3); } } } + private static void createStoneGateway(World world, int x, int y, int z, Random random) + { + final int blockID = Block.stoneBrick.blockID; + + //Replace some of the ground around the gateway with bricks + for (int xc = -GATEWAY_RADIUS; xc <= GATEWAY_RADIUS; xc++) + { + for (int zc= -GATEWAY_RADIUS; zc <= GATEWAY_RADIUS; zc++) + { + //Check that the block is supported by an opaque block. + //This prevents us from building over a cliff, on the peak of a mountain, + //or the surface of the ocean or a frozen lake. + if (world.isBlockOpaqueCube(x + xc, y - 2, z + zc)) + { + //Randomly choose whether to place bricks or not. The math is designed so that the + //chances of placing a block decrease as we get farther from the gateway's center. + if (Math.abs(xc) + Math.abs(zc) < random.nextInt(2) + 3) + { + //Place Stone Bricks + world.setBlock(x + xc, y - 1, z + zc, blockID, 0, 3); + } + else if (Math.abs(xc) + Math.abs(zc) < random.nextInt(3) + 3) + { + //Place Cracked Stone Bricks + world.setBlock(x + xc, y - 1, z + zc, blockID, 2, 3); + } + } + } + } + + //Use Chiseled Stone Bricks to top off the pillars around the door + world.setBlock(x, y + 2, z + 1, blockID, 3, 3); + world.setBlock(x, y + 2, z - 1, blockID, 3, 3); + //Build the columns around the door + world.setBlock(x, y + 1, z - 1, blockID, 0, 3); + world.setBlock(x, y + 1, z + 1, blockID, 0, 3); + world.setBlock(x, y, z - 1, blockID, 0, 3); + world.setBlock(x, y, z + 1, blockID, 0, 3); + } + + private static void createLimboGateway(World world, int x, int y, int z) + { + //Build the gateway out of Unraveled Fabric. Since nearly all the blocks in Limbo are of + //that type, there is no point replacing the ground. + final int blockID = properties.LimboBlockID; + world.setBlock(x, y + 2, z + 1, blockID, 0, 3); + world.setBlock(x, y + 2, z - 1, blockID, 0, 3); + + //Build the columns around the door + world.setBlock(x, y + 1, z - 1, blockID, 0, 3); + world.setBlock(x, y + 1, z + 1, blockID, 0, 3); + world.setBlock(x, y, z - 1, blockID, 0, 3); + world.setBlock(x, y, z + 1, blockID, 0, 3); + } + private static boolean checkGatewayLocation(World world, int x, int y, int z) { //Check if the point is within the acceptable altitude range, the block above that point is empty, @@ -195,6 +228,6 @@ public class RiftGenerator implements IWorldGenerator private static boolean isStructureGenerationAllowed() { - return DimensionManager.getWorld(0).getWorldInfo().isMapFeaturesEnabled(); + return DimensionManager.getWorld(OVERWORLD_DIMENSION_ID).getWorldInfo().isMapFeaturesEnabled(); } } diff --git a/StevenDimDoors/mod_pocketDim/SchematicLoader.java b/StevenDimDoors/mod_pocketDim/SchematicLoader.java index 279a297..f5006f4 100644 --- a/StevenDimDoors/mod_pocketDim/SchematicLoader.java +++ b/StevenDimDoors/mod_pocketDim/SchematicLoader.java @@ -2,6 +2,7 @@ package StevenDimDoors.mod_pocketDim; import java.io.File; import java.io.FileNotFoundException; import java.util.HashMap; +import java.util.Random; import net.minecraft.world.World; import StevenDimDoors.mod_pocketDim.dungeon.DungeonSchematic; @@ -27,14 +28,28 @@ public class SchematicLoader int originDimID = link.locDimID; int destDimID = link.destDimID; HashMap dimList = dimHelper.dimList; + DungeonHelper dungeonHelper = DungeonHelper.instance(); + World world; if (dimList.containsKey(destDimID)) { + dimList.get(destDimID).hasBeenFilled = true; + if (dimHelper.getWorld(destDimID) == null) + { + dimHelper.initDimension(destDimID); + } + world = dimHelper.getWorld(destDimID); + if (dimList.get(destDimID).dungeonGenerator == null) { - DungeonHelper.instance().generateDungeonLink(link); + //TODO: We should centralize RNG initialization and world-seed modifiers for each specific application. + final long localSeed = world.getSeed() ^ 0x2F50DB9B4A8057E4L ^ computeDestinationHash(link); + final Random random = new Random(localSeed); + + dungeonHelper.generateDungeonLink(link, dungeonHelper.RuinsPack, random); } schematicPath = dimList.get(destDimID).dungeonGenerator.schematicPath; + } else { @@ -71,19 +86,12 @@ public class SchematicLoader //TODO: In the future, remove this dungeon from the generation lists altogether. //That will have to wait until our code is updated to support that more easily. System.err.println("The dungeon will not be loaded."); - DungeonGenerator defaultError = DungeonHelper.instance().getDefaultErrorDungeon(); + DungeonGenerator defaultError = dungeonHelper.getDefaultErrorDungeon(); dimList.get(destDimID).dungeonGenerator = defaultError; dungeon = checkSourceAndLoad(defaultError.schematicPath); dungeon.applyImportFilters(properties); } - dimList.get(destDimID).hasBeenFilled = true; - if (dimHelper.getWorld(destDimID) == null) - { - dimHelper.initDimension(destDimID); - } - World world = dimHelper.getWorld(destDimID); - //Adjust the height at which the dungeon is placed to prevent vertical clipping int fixedY = adjustDestinationY(world, link.destYCoord, dungeon); if (fixedY != link.destYCoord) @@ -145,4 +153,44 @@ public class SchematicLoader } return dungeon; } + + private static long computeDestinationHash(LinkData link) + { + //Time for some witchcraft. + //The code here is inspired by a discussion on Stack Overflow regarding hash codes for 3D. + //Source: http://stackoverflow.com/questions/9858376/hashcode-for-3d-integer-coordinates-with-high-spatial-coherence + + //Use 8 bits from Y and 24 bits from X and Z. Mix in 8 bits from the destination dim ID too - that means + //even if you aligned two doors perfectly between two pockets, it's unlikely they would lead to the same dungeon. + + int bit; + int index; + long hash; + int w = link.destDimID; + int x = link.destXCoord; + int y = link.destYCoord; + int z = link.destZCoord; + + hash = 0; + index = 0; + for (bit = 0; bit < 8; bit++) + { + hash |= ((w >> bit) & 1) << index; + index++; + hash |= ((x >> bit) & 1) << index; + index++; + hash |= ((y >> bit) & 1) << index; + index++; + hash |= ((z >> bit) & 1) << index; + index++; + } + for (; bit < 24; bit++) + { + hash |= ((x >> bit) & 1) << index; + index++; + hash |= ((z >> bit) & 1) << index; + index++; + } + return hash; + } } \ No newline at end of file diff --git a/StevenDimDoors/mod_pocketDim/commands/CommandExportDungeon.java b/StevenDimDoors/mod_pocketDim/commands/CommandExportDungeon.java index ff833b3..194d5ce 100644 --- a/StevenDimDoors/mod_pocketDim/commands/CommandExportDungeon.java +++ b/StevenDimDoors/mod_pocketDim/commands/CommandExportDungeon.java @@ -54,7 +54,7 @@ public class CommandExportDungeon extends DDCommandBase if (command[1].equalsIgnoreCase("override")) { //Check that the schematic name is a legal name - if (DungeonHelper.SchematicNamePattern.matcher(command[0]).matches()) + if (DungeonHelper.SCHEMATIC_NAME_PATTERN.matcher(command[0]).matches()) { //Export the schematic return exportDungeon(sender, command[0]); @@ -85,7 +85,7 @@ public class CommandExportDungeon extends DDCommandBase { return new DDCommandResult("Error: Invalid dungeon type. Please use one of the existing types."); } - if (!DungeonHelper.DungeonNamePattern.matcher(command[1]).matches()) + if (!DungeonHelper.DUNGEON_NAME_PATTERN.matcher(command[1]).matches()) { return new DDCommandResult("Error: Invalid dungeon name. Please use only letters, numbers, and dashes."); } diff --git a/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonChainRule.java b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonChainRule.java new file mode 100644 index 0000000..17554dc --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonChainRule.java @@ -0,0 +1,67 @@ +package StevenDimDoors.mod_pocketDim.dungeon.pack; + +import java.util.ArrayList; +import java.util.HashMap; + +import StevenDimDoors.mod_pocketDim.util.WeightedContainer; + +public class DungeonChainRule +{ + private final int[] condition; + private final ArrayList> products; + + public DungeonChainRule(DungeonChainRuleDefinition source, HashMap nameToTypeMapping) + { + ArrayList conditionNames = source.getCondition(); + ArrayList> productNames = source.getProducts(); + + //Obtain the IDs of dungeon types in reverse order. Reverse order makes comparing against chain histories easy. + condition = new int[conditionNames.size()]; + for (int src = 0, dst = condition.length - 1; src < condition.length; src++, dst--) + { + condition[dst] = nameToTypeMapping.get(conditionNames.get(src)).ID; + } + products = new ArrayList>(productNames.size()); + for (WeightedContainer product : productNames) + { + products.add(new WeightedContainer(nameToTypeMapping.get(product.getData()), product.itemWeight )); + } + } + + public int length() + { + return condition.length; + } + + public boolean evaluate(int[] typeHistory) + { + if (typeHistory.length >= condition.length) + { + for (int k = 0; k < condition.length; k++) + { + if (condition[k] != 0 && typeHistory[k] != condition[k]) + { + return false; + } + } + return true; + } + else + { + return false; + } + } + + public ArrayList> products() + { + //Create a deep copy of the internal list of products. That way, if the list is modified externally, + //it won't affect the reference copy inside this rule. + ArrayList> copy = new ArrayList>(products.size()); + for (WeightedContainer container : products) + { + copy.add(container.clone()); + } + + return copy; + } +} diff --git a/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonChainRuleDefinition.java b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonChainRuleDefinition.java new file mode 100644 index 0000000..f06cded --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonChainRuleDefinition.java @@ -0,0 +1,28 @@ +package StevenDimDoors.mod_pocketDim.dungeon.pack; + +import java.util.ArrayList; + +import StevenDimDoors.mod_pocketDim.util.WeightedContainer; + +public class DungeonChainRuleDefinition +{ + private ArrayList conditions; + private ArrayList> products; + + public DungeonChainRuleDefinition(ArrayList conditions, ArrayList> products) + { + this.conditions = conditions; + this.products = products; + } + + public ArrayList getCondition() + { + return conditions; + } + + public ArrayList> getProducts() + { + return products; + } + +} diff --git a/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonPack.java b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonPack.java new file mode 100644 index 0000000..40bc2c7 --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonPack.java @@ -0,0 +1,258 @@ +package StevenDimDoors.mod_pocketDim.dungeon.pack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Random; + +import net.minecraft.util.WeightedRandom; +import StevenDimDoors.mod_pocketDim.DungeonGenerator; +import StevenDimDoors.mod_pocketDim.LinkData; +import StevenDimDoors.mod_pocketDim.helpers.DungeonHelper; +import StevenDimDoors.mod_pocketDim.helpers.dimHelper; +import StevenDimDoors.mod_pocketDim.util.WeightedContainer; + +public class DungeonPack +{ + //There is no precaution against having a dungeon type removed from a config file after dungeons of that type + //have been generated. That would likely cause one or two problems. It's hard to guard against when I don't know + //what the save format will be like completely. How should this class behave if it finds a "disowned" type? + //The ID numbers would be a problem since it couldn't have a valid number, since it wasn't initialized by the pack instance. + //FIXME: Do not release this code as an update without dealing with disowned types! + + private final String name; + private final HashMap nameToTypeMapping; + private final ArrayList> groupedDungeons; + private final ArrayList allDungeons; + private final DungeonPackConfig config; + private final int maxRuleLength; + private final ArrayList rules; + + public DungeonPack(DungeonPackConfig config) + { + config.validate(); + this.config = config.clone(); //Store a clone of the config so that the caller can't change it externally later + this.name = config.getName(); + + int index; + int maxLength = 0; + int typeCount = config.getTypeNames().size(); + this.allDungeons = new ArrayList(); + this.nameToTypeMapping = new HashMap(typeCount); + this.groupedDungeons = new ArrayList>(typeCount); + + this.groupedDungeons.add(allDungeons); //Make sure the list of all dungeons is placed at index 0 + this.nameToTypeMapping.put(DungeonType.WILDCARD_TYPE.Name, DungeonType.WILDCARD_TYPE); + + index = 1; + for (String typeName : config.getTypeNames()) + { + String standardName = typeName.toUpperCase(); + this.nameToTypeMapping.put(standardName, new DungeonType(this, standardName, index)); + this.groupedDungeons.add(new ArrayList()); + index++; + } + + //Construct optimized rules from definitions + ArrayList definitions = config.getRules(); + this.rules = new ArrayList(definitions.size()); + for (DungeonChainRuleDefinition definition : definitions) + { + DungeonChainRule rule = new DungeonChainRule(definition, nameToTypeMapping); + this.rules.add(rule); + if (maxLength < rule.length()) + { + maxLength = rule.length(); + } + } + this.maxRuleLength = maxLength; + + //Remove unnecessary references to save a little memory - we won't need them here + this.config.setRules(null); + this.config.setTypeNames(null); + } + + public String getName() + { + return name; + } + + public boolean isEmpty() + { + return allDungeons.isEmpty(); + } + + public DungeonType getType(String typeName) + { + DungeonType result = nameToTypeMapping.get(typeName.toUpperCase()); + if (result.Owner == this) //Filter out the wildcard dungeon type + { + return result; + } + else + { + return null; + } + } + + public boolean isKnownType(String typeName) + { + return (this.getType(typeName) != null); + } + + public void addDungeon(DungeonGenerator generator) + { + //Make sure this dungeon really belongs in this pack + DungeonType type = generator.getDungeonType(); + if (type.Owner == this) + { + allDungeons.add(generator); + groupedDungeons.get(type.ID).add(generator); + } + else + { + throw new IllegalArgumentException("The dungeon type of generator must belong to this instance of DungeonPack."); + } + } + + public DungeonGenerator getNextDungeon(LinkData inbound, Random random) + { + if (allDungeons.isEmpty()) + { + return null; + } + + //Retrieve a list of the previous dungeons in this chain. + //If we're not going to check for duplicates in chains, restrict the length of the history to the length + //of the longest rule we have. Getting any more data would be useless. This optimization could be significant + //for dungeon packs that can extend arbitrarily deep. We should probably set a reasonable limit anyway. + + int maxSearchLength = config.allowDuplicatesInChain() ? maxRuleLength : 1337; + ArrayList history = DungeonHelper.getDungeonChainHistory( + dimHelper.instance.getDimData(inbound.locDimID), this, maxSearchLength); + return getNextDungeon(history, random); + } + + private DungeonGenerator getNextDungeon(ArrayList history, Random random) + { + //Extract the dungeon types that have been used from history and convert them into an array of IDs + int index; + int[] typeHistory = new int[history.size()]; + HashSet excludedDungeons = null; + for (index = 0; index < typeHistory.length; index++) + { + typeHistory[index] = history.get(index).getDungeonType().ID; + } + + for (DungeonChainRule rule : rules) + { + if (rule.evaluate(typeHistory)) + { + //Pick a random dungeon type to be generated next based on the rule's products + ArrayList> products = rule.products(); + DungeonType nextType; + do + { + nextType = getRandomDungeonType(random, products, groupedDungeons); + if (nextType != null) + { + //Initialize the set of excluded dungeons if needed + if (excludedDungeons == null && !config.allowDuplicatesInChain()) + { + excludedDungeons = new HashSet(history); + } + + //List which dungeons are allowed + ArrayList candidates; + ArrayList group = groupedDungeons.get(nextType.ID); + if (excludedDungeons != null && !excludedDungeons.isEmpty()) + { + candidates = new ArrayList(group.size()); + for (DungeonGenerator dungeon : group) + { + if (!excludedDungeons.contains(dungeon)) + { + candidates.add(dungeon); + } + } + } + else + { + candidates = group; + } + if (!candidates.isEmpty()) + { + return getRandomDungeon(random, candidates); + } + //If we've reached this point, then a dungeon was not selected. Discard the type and try again. + products.remove(nextType); + } + } + while (nextType != null); + } + } + + //None of the rules were applicable. Simply return a random dungeon. + return getRandomDungeon(random); + } + + public DungeonGenerator getRandomDungeon(Random random) + { + if (!allDungeons.isEmpty()) + { + return getRandomDungeon(random, allDungeons); + } + else + { + return null; + } + } + + private static DungeonType getRandomDungeonType(Random random, Collection> types, + ArrayList> groupedDungeons) + { + //TODO: Make this faster? This algorithm runs in quadratic time in the worst case because of the random-selection + //process and the removal search. Might be okay for normal use, though. ~SenseiKiwi + + //Pick a random dungeon type based on weights. Repeat this process until a non-empty group is found or all groups are checked. + while (!types.isEmpty()) + { + //Pick a random dungeon type + @SuppressWarnings("unchecked") + WeightedContainer resultContainer = (WeightedContainer) WeightedRandom.getRandomItem(random, types); + + //Check if there are any dungeons of that type + DungeonType selectedType = resultContainer.getData(); + if (!groupedDungeons.get(selectedType.ID).isEmpty()) + { + //Choose this type + return selectedType; + } + else + { + //We can't use this type because there are no dungeons of this type + //Remove it from the list of types and try again + types.remove(resultContainer); + } + } + + //We have run out of types to try + return null; + } + + private static DungeonGenerator getRandomDungeon(Random random, Collection dungeons) + { + //Use Minecraft's WeightedRandom to select our dungeon. =D + ArrayList> weights = + new ArrayList>(dungeons.size()); + for (DungeonGenerator dungeon : dungeons) + { + weights.add(new WeightedContainer(dungeon, dungeon.weight)); + } + + @SuppressWarnings("unchecked") + WeightedContainer resultContainer = (WeightedContainer) WeightedRandom.getRandomItem(random, weights); + return (resultContainer != null) ? resultContainer.getData() : null; + } +} diff --git a/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonPackConfig.java b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonPackConfig.java new file mode 100644 index 0000000..842c94c --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonPackConfig.java @@ -0,0 +1,126 @@ +package StevenDimDoors.mod_pocketDim.dungeon.pack; + +import java.util.ArrayList; + +public class DungeonPackConfig +{ + private String name; + private ArrayList typeNames; + private boolean allowDuplicatesInChain; + private boolean allowPackChangeIn; + private boolean allowPackChangeOut; + private boolean distortDoorCoordinates; + private int packWeight; + private ArrayList rules; + + public DungeonPackConfig() { } + + @SuppressWarnings("unchecked") + private DungeonPackConfig(DungeonPackConfig source) + { + this.name = source.name; + this.typeNames = (ArrayList) 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) source.rules.clone(); + } + + public void validate() + { + if (this.name == null) + throw new NullPointerException("name cannot be null"); + if (this.typeNames == null) + throw new NullPointerException("typeNames cannot be null"); + if (this.rules == null) + throw new NullPointerException("rules cannot be null"); + } + + @Override + public DungeonPackConfig clone() + { + return new DungeonPackConfig(this); + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public ArrayList getTypeNames() + { + return typeNames; + } + + public void setTypeNames(ArrayList typeNames) + { + this.typeNames = typeNames; + } + + public boolean allowDuplicatesInChain() + { + return allowDuplicatesInChain; + } + + public void setAllowDuplicatesInChain(boolean value) + { + allowDuplicatesInChain = value; + } + + public void setRules(ArrayList rules) + { + this.rules = rules; + } + + public ArrayList getRules() + { + 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; + } +} diff --git a/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonPackConfigReader.java b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonPackConfigReader.java new file mode 100644 index 0000000..8aac967 --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonPackConfigReader.java @@ -0,0 +1,385 @@ +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 +{ + 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()); + config.setRules(new ArrayList()); + + //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 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 condition; + ArrayList> products; + List 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(); + products = new ArrayList>(); + + 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(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 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."); + } +} diff --git a/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonType.java b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonType.java new file mode 100644 index 0000000..50e5921 --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/dungeon/pack/DungeonType.java @@ -0,0 +1,48 @@ +package StevenDimDoors.mod_pocketDim.dungeon.pack; + +public class DungeonType implements Comparable +{ + public static final DungeonType WILDCARD_TYPE = new DungeonType(null, "?", 0); + public static final DungeonType UNKNOWN_TYPE = new DungeonType(null, "!", -1); + + public final DungeonPack Owner; + public final String Name; + public final int ID; + + public DungeonType(DungeonPack owner, String name, int id) + { + Owner = owner; + Name = name; + this.ID = id; + } + + @Override + public int compareTo(DungeonType other) + { + return this.ID - other.ID; + } + + @Override + public boolean equals(Object other) + { + return equals((DungeonType) other); + } + + public boolean equals(DungeonType other) + { + if (this == other) + return true; + + if (this == null || other == null) + return false; + + return (this.ID == other.ID); + } + + @Override + public int hashCode() + { + final int prime = 2039; + return prime * ID; + } +} diff --git a/StevenDimDoors/mod_pocketDim/helpers/DungeonHelper.java b/StevenDimDoors/mod_pocketDim/helpers/DungeonHelper.java index 25b4156..0050a9f 100644 --- a/StevenDimDoors/mod_pocketDim/helpers/DungeonHelper.java +++ b/StevenDimDoors/mod_pocketDim/helpers/DungeonHelper.java @@ -7,13 +7,13 @@ import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; import java.util.Random; import java.util.regex.Pattern; -import net.minecraft.util.WeightedRandom; import net.minecraft.world.World; import StevenDimDoors.mod_pocketDim.DDProperties; import StevenDimDoors.mod_pocketDim.DimData; @@ -21,15 +21,20 @@ 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.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 { private static DungeonHelper instance = null; private static DDProperties properties = null; - public static final Pattern SchematicNamePattern = Pattern.compile("[A-Za-z0-9_\\-]+"); - public static final Pattern DungeonNamePattern = Pattern.compile("[A-Za-z0-9\\-]+"); + + public static final Pattern SCHEMATIC_NAME_PATTERN = Pattern.compile("[A-Za-z0-9_\\-]+"); + public static final Pattern DUNGEON_NAME_PATTERN = Pattern.compile("[A-Za-z0-9\\-]+"); private static final String DEFAULT_UP_SCHEMATIC_PATH = "/schematics/core/simpleStairsUp.schematic"; private static final String DEFAULT_DOWN_SCHEMATIC_PATH = "/schematics/core/simpleStairsDown.schematic"; @@ -45,72 +50,22 @@ public class DungeonHelper public static final short MAX_DUNGEON_HEIGHT = MAX_DUNGEON_WIDTH; public static final short MAX_DUNGEON_LENGTH = MAX_DUNGEON_WIDTH; - private static final String HUB_DUNGEON_TYPE = "Hub"; - private static final String TRAP_DUNGEON_TYPE = "Trap"; - private static final String SIMPLE_HALL_DUNGEON_TYPE = "SimpleHall"; - private static final String COMPLEX_HALL_DUNGEON_TYPE = "ComplexHall"; - private static final String EXIT_DUNGEON_TYPE = "Exit"; - private static final String DEAD_END_DUNGEON_TYPE = "DeadEnd"; - private static final String MAZE_DUNGEON_TYPE = "Maze"; - - //The list of dungeon types will be kept as an array for now. If we allow new - //dungeon types in the future, then this can be changed to an ArrayList. - private static final String[] DUNGEON_TYPES = new String[] { - HUB_DUNGEON_TYPE, - TRAP_DUNGEON_TYPE, - SIMPLE_HALL_DUNGEON_TYPE, - COMPLEX_HALL_DUNGEON_TYPE, - EXIT_DUNGEON_TYPE, - DEAD_END_DUNGEON_TYPE, - MAZE_DUNGEON_TYPE - }; - - private Random rand = new Random(); - private ArrayList untaggedDungeons = new ArrayList(); private ArrayList registeredDungeons = new ArrayList(); - - private ArrayList simpleHalls = new ArrayList(); - private ArrayList complexHalls = new ArrayList(); - private ArrayList deadEnds = new ArrayList(); - private ArrayList hubs = new ArrayList(); - private ArrayList mazes = new ArrayList(); - private ArrayList pistonTraps = new ArrayList(); - private ArrayList exits = new ArrayList(); + public DungeonPack RuinsPack; + private DungeonGenerator defaultUp; private DungeonGenerator defaultDown; private DungeonGenerator defaultError; - private HashSet dungeonTypeChecker; - private HashMap> dungeonTypeMapping; - private DungeonHelper() { - //Load the dungeon type checker with the list of all types in lowercase. - //Capitalization matters for matching in a hash set. - dungeonTypeChecker = new HashSet(); - for (String dungeonType : DUNGEON_TYPES) - { - dungeonTypeChecker.add(dungeonType.toLowerCase()); - } - - //Add all the basic dungeon types to dungeonTypeMapping - //Dungeon type names must be passed in lowercase to make matching easier. - dungeonTypeMapping = new HashMap>(); - dungeonTypeMapping.put(SIMPLE_HALL_DUNGEON_TYPE.toLowerCase(), simpleHalls); - dungeonTypeMapping.put(COMPLEX_HALL_DUNGEON_TYPE.toLowerCase(), complexHalls); - dungeonTypeMapping.put(HUB_DUNGEON_TYPE.toLowerCase(), hubs); - dungeonTypeMapping.put(EXIT_DUNGEON_TYPE.toLowerCase(), exits); - dungeonTypeMapping.put(DEAD_END_DUNGEON_TYPE.toLowerCase(), deadEnds); - dungeonTypeMapping.put(MAZE_DUNGEON_TYPE.toLowerCase(), mazes); - dungeonTypeMapping.put(TRAP_DUNGEON_TYPE.toLowerCase(), pistonTraps); - //Load our reference to the DDProperties singleton if (properties == null) properties = DDProperties.instance(); - registerCustomDungeons(); + registerDungeons(); } public static DungeonHelper initialize() @@ -138,15 +93,39 @@ public class DungeonHelper return instance; } - private void registerCustomDungeons() + private void registerDungeons() { File file = new File(properties.CustomSchematicDirectory); if (file.exists() || file.mkdir()) { copyfile.copyFile(DUNGEON_CREATION_GUIDE_SOURCE_PATH, file.getAbsolutePath() + "/How_to_add_dungeons.txt"); } + + RuinsPack = new DungeonPack(createRuinsConfig()); + registerBundledDungeons(); - importCustomDungeons(properties.CustomSchematicDirectory); + registerCustomDungeons(properties.CustomSchematicDirectory); + } + + private static DungeonPackConfig createRuinsConfig() + { + //This is a temporarily function for testing dungeon packs. + //It'll be removed later when we read dungeon configurations from files. + + DungeonPackConfig config; + try + { + config = (new DungeonPackConfigReader()).readFromResource("/schematics/ruins/rules.txt"); + config.setName("ruins"); + return config; + } + catch (ConfigurationProcessingException e) + { + //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; + } } public List getRegisteredDungeons() @@ -189,7 +168,7 @@ public class DungeonHelper public boolean validateDungeonType(String type) { //Check if the dungeon type is valid - return dungeonTypeChecker.contains(type.toLowerCase()); + return RuinsPack.isKnownType(type); } public boolean validateSchematicName(String name) @@ -206,11 +185,11 @@ public class DungeonHelper return false; //Check if the dungeon type is valid - if (!dungeonTypeChecker.contains(dungeonData[0].toLowerCase())) + if (!validateDungeonType(dungeonData[0])) return false; //Check if the name is valid - if (!SchematicNamePattern.matcher(dungeonData[1]).matches()) + if (!SCHEMATIC_NAME_PATTERN.matcher(dungeonData[1]).matches()) return false; //Check if the open/closed flag is present @@ -249,14 +228,14 @@ public class DungeonHelper //Strip off the file extension while splitting the file name String[] dungeonData = name.substring(0, name.length() - SCHEMATIC_FILE_EXTENSION.length()).split("_"); - String dungeonType = dungeonData[0].toLowerCase(); + DungeonType dungeonType = RuinsPack.getType(dungeonData[0]); boolean isOpen = dungeonData[2].equalsIgnoreCase("open"); int weight = (dungeonData.length == 4) ? Integer.parseInt(dungeonData[3]) : DEFAULT_DUNGEON_WEIGHT; //Add this custom dungeon to the list corresponding to its type - DungeonGenerator generator = new DungeonGenerator(weight, path, isOpen); + DungeonGenerator generator = new DungeonGenerator(weight, path, isOpen, dungeonType); - dungeonTypeMapping.get(dungeonType).add(generator); + RuinsPack.addDungeon(generator); registeredDungeons.add(generator); if (verbose) { @@ -269,7 +248,7 @@ public class DungeonHelper { System.out.println("Could not parse dungeon filename, not adding dungeon to generation lists"); } - untaggedDungeons.add(new DungeonGenerator(DEFAULT_DUNGEON_WEIGHT, path, true)); + untaggedDungeons.add(new DungeonGenerator(DEFAULT_DUNGEON_WEIGHT, path, true, DungeonType.UNKNOWN_TYPE)); System.out.println("Registered untagged dungeon: " + name); } } @@ -280,7 +259,7 @@ public class DungeonHelper } } - private void importCustomDungeons(String path) + private void registerCustomDungeons(String path) { File directory = new File(path); File[] schematicNames = directory.listFiles(); @@ -301,9 +280,9 @@ public class DungeonHelper { //Register the core schematics //These are used for debugging and in case of unusual errors while loading dungeons - defaultUp = new DungeonGenerator(DEFAULT_DUNGEON_WEIGHT, DEFAULT_UP_SCHEMATIC_PATH, true); - defaultDown = new DungeonGenerator(DEFAULT_DUNGEON_WEIGHT, DEFAULT_DOWN_SCHEMATIC_PATH, true); - defaultError = new DungeonGenerator(DEFAULT_DUNGEON_WEIGHT, DEFAULT_ERROR_SCHEMATIC_PATH, true); + defaultUp = new DungeonGenerator(DEFAULT_DUNGEON_WEIGHT, DEFAULT_UP_SCHEMATIC_PATH, true, DungeonType.UNKNOWN_TYPE); + defaultDown = new DungeonGenerator(DEFAULT_DUNGEON_WEIGHT, DEFAULT_DOWN_SCHEMATIC_PATH, true, DungeonType.UNKNOWN_TYPE); + defaultError = new DungeonGenerator(DEFAULT_DUNGEON_WEIGHT, DEFAULT_ERROR_SCHEMATIC_PATH, true, DungeonType.UNKNOWN_TYPE); //Open the list of dungeons packaged with our mod and register their schematics InputStream listStream = this.getClass().getResourceAsStream(BUNDLED_DUNGEONS_LIST_PATH); @@ -354,157 +333,29 @@ public class DungeonHelper } } - public void generateDungeonLink(LinkData incoming) + public void generateDungeonLink(LinkData inbound, DungeonPack pack, Random random) { - DungeonGenerator dungeon; - int depth = dimHelper.instance.getDimDepth(incoming.locDimID); - int depthWeight = rand.nextInt(depth + 2) + rand.nextInt(depth + 2) - 2; - - int count = 10; - boolean flag = true; + DungeonGenerator selection; + try - { - - if (incoming.destYCoord > 15) - { - do - { - count--; - flag = true; - //Select a dungeon at random, taking into account its weight - dungeon = getRandomDungeon(rand, registeredDungeons); - - if (depth <= 1) - { - if(rand.nextBoolean()) - { - dungeon = complexHalls.get(rand.nextInt(complexHalls.size())); - - } - else if(rand.nextBoolean()) - { - dungeon = hubs.get(rand.nextInt(hubs.size())); - - } - else if(rand.nextBoolean()) - { - dungeon = hubs.get(rand.nextInt(hubs.size())); - - } - else if(deadEnds.contains(dungeon)||exits.contains(dungeon)) - { - flag=false; - } - } - else if (depth <= 3 && (deadEnds.contains(dungeon) || exits.contains(dungeon) || rand.nextBoolean())) - { - if(rand.nextBoolean()) - { - dungeon = hubs.get(rand.nextInt(hubs.size())); - - } - else if(rand.nextBoolean()) - { - dungeon = mazes.get(rand.nextInt(mazes.size())); - } - else if(rand.nextBoolean()) - { - dungeon = pistonTraps.get(rand.nextInt(pistonTraps.size())); - - } - else - { - flag = false; - } - } - else if (rand.nextInt(3) == 0 && !complexHalls.contains(dungeon)) - { - if (rand.nextInt(3) == 0) - { - dungeon = simpleHalls.get(rand.nextInt(simpleHalls.size())); - } - else if(rand.nextBoolean()) - { - dungeon = pistonTraps.get(rand.nextInt(pistonTraps.size())); - } - else if (depth < 4) - { - dungeon = hubs.get(rand.nextInt(hubs.size())); - } - } - else if (depthWeight - depthWeight / 2 > depth -4 && (deadEnds.contains(dungeon) || exits.contains(dungeon))) - { - if(rand.nextBoolean()) - { - dungeon = simpleHalls.get(rand.nextInt(simpleHalls.size())); - } - else if(rand.nextBoolean()) - { - dungeon = complexHalls.get(rand.nextInt(complexHalls.size())); - } - else if(rand.nextBoolean()) - { - dungeon = pistonTraps.get(rand.nextInt(pistonTraps.size())); - } - else - { - flag = false; - } - } - else if (depthWeight > 7 && hubs.contains(dungeon)) - { - if(rand.nextInt(12)+5 10 && hubs.contains(dungeon)) - { - flag = false; - } - - if(getDungeonDataInChain(dimHelper.instance.getDimData(incoming.locDimID)).contains(dungeon)) - { - flag=false; - } - } - while (!flag && count > 0); - } - else - { - dungeon = defaultUp; - } + { + selection = pack.getNextDungeon(inbound, random); } catch (Exception e) { + System.err.println("An exception occurred while selecting a dungeon:"); e.printStackTrace(); - if (registeredDungeons.size() > 0) + + if (!pack.isEmpty()) { - //Select a random dungeon - dungeon = getRandomDungeon(rand, registeredDungeons); + selection = pack.getRandomDungeon(random); } else { - return; + selection = defaultError; } } - dimHelper.instance.getDimData(incoming.destDimID).dungeonGenerator = dungeon; - //dimHelper.instance.getDimData(incoming.destDimID).dungeonGenerator = defaultUp; + dimHelper.instance.getDimData(inbound.destDimID).dungeonGenerator = selection; } public Collection getDungeonNames() { @@ -539,50 +390,78 @@ public class DungeonHelper return names; } - private static DungeonGenerator getRandomDungeon(Random random, Collection dungeons) + public static ArrayList getDungeonChainHistory(DimData dimData, DungeonPack pack, int maxSize) { - //Use Minecraft's WeightedRandom to select our dungeon. =D - ArrayList> weights = - new ArrayList>(dungeons.size()); - for (DungeonGenerator dungeon : dungeons) - { - weights.add(new WeightedContainer(dungeon, dungeon.weight)); - } + //TODO: I've improved this code for the time being. However, searching across links is a flawed approach. A player could + //manipulate the output of this function by setting up links to mislead our algorithm or by removing links. + //Dimensions MUST have built-in records of their parent dimensions in the future. ~SenseiKiwi - @SuppressWarnings("unchecked") - WeightedContainer resultContainer = (WeightedContainer) WeightedRandom.getRandomItem(random, weights); - return (resultContainer != null) ? resultContainer.getData() : null; - } - public static ArrayList getDungeonDataInChain(DimData dimData) - { - DimData startingDim = dimHelper.instance.getDimData(dimHelper.instance.getLinkDataFromCoords(dimData.exitDimLink.destXCoord, dimData.exitDimLink.destYCoord, dimData.exitDimLink.destZCoord, dimData.exitDimLink.destDimID).destDimID); - - return getDungeonDataBelow(startingDim); - } - private static ArrayList getDungeonDataBelow(DimData dimData) - { - ArrayList dungeonData = new ArrayList(); - if(dimData.dungeonGenerator!=null) + ArrayList history = new ArrayList(); + DimData tailDim = dimData; + boolean found = true; + + if (dimData.dungeonGenerator == null || dimData.dungeonGenerator.getDungeonType().Owner != pack || maxSize < 1) { - dungeonData.add(dimData.dungeonGenerator); - - for(LinkData link : dimData.getLinksInDim()) + //The initial dimension is already outside our pack. Return an empty list. + return history; + } + history.add(dimData.dungeonGenerator); + + for (int count = 1; count < maxSize && found; count++) + { + found = false; + for (LinkData link : tailDim.getLinksInDim()) { - if(dimHelper.dimList.containsKey(link.destDimID)) + DimData neighbor = dimHelper.instance.getDimData(link.destDimID); + if (neighbor.depth == tailDim.depth - 1 && neighbor.dungeonGenerator != null && + neighbor.dungeonGenerator.getDungeonType().Owner == pack) { - if(dimHelper.instance.getDimData(link.destDimID).dungeonGenerator!=null&&dimHelper.instance.getDimDepth(link.destDimID)==dimData.depth+1) - { - for(DungeonGenerator dungeonGen :getDungeonDataBelow(dimHelper.instance.getDimData(link.destDimID)) ) - { - if(!dungeonData.contains(dungeonGen)) - { - dungeonData.add(dungeonGen); - } - } - } + tailDim = neighbor; + history.add(tailDim.dungeonGenerator); + found = true; + break; } } } - return dungeonData; + return history; + } + + public static ArrayList getFlatDungeonTree(DimData dimData, int maxSize) + { + //TODO: I've improved this code for the time being. However, searching across links is a flawed approach. A player could + //manipulate the output of this function by setting up links to mislead our algorithm or by removing links. + //Dimensions MUST have built-in records of their parent dimensions in the future. ~SenseiKiwi + + dimHelper helper = dimHelper.instance; + ArrayList dungeons = new ArrayList(); + DimData root = helper.getDimData(helper.getLinkDataFromCoords(dimData.exitDimLink.destXCoord, dimData.exitDimLink.destYCoord, dimData.exitDimLink.destZCoord, dimData.exitDimLink.destDimID).destDimID); + HashSet checked = new HashSet(); + Queue pendingDimensions = new LinkedList(); + + if (root.dungeonGenerator == null) + { + return dungeons; + } + pendingDimensions.add(root); + checked.add(root); + + while (dungeons.size() < maxSize && !pendingDimensions.isEmpty()) + { + DimData current = pendingDimensions.remove(); + for (LinkData link : current.getLinksInDim()) + { + DimData child = helper.getDimData(link.destDimID); + if (child.depth == current.depth + 1 && child.dungeonGenerator != null && checked.add(child)) + { + dungeons.add(child.dungeonGenerator); + pendingDimensions.add(child); + } + if (dungeons.size() == maxSize) + { + break; + } + } + } + return dungeons; } } \ No newline at end of file diff --git a/StevenDimDoors/mod_pocketDim/helpers/dimHelper.java b/StevenDimDoors/mod_pocketDim/helpers/dimHelper.java index d4325f2..5e7bf63 100644 --- a/StevenDimDoors/mod_pocketDim/helpers/dimHelper.java +++ b/StevenDimDoors/mod_pocketDim/helpers/dimHelper.java @@ -1317,17 +1317,9 @@ public class dimHelper extends DimensionManager { return dimHelper.instance.getDimData(world.provider.dimensionId); } + public DimData getDimData(int dimID) { - if(dimHelper.dimList.containsKey(dimID)) - { - return dimHelper.dimList.get(dimID); - } - else - { - return null; - } + return dimHelper.dimList.get(dimID); } - - } diff --git a/StevenDimDoors/mod_pocketDim/util/BaseConfigurationProcessor.java b/StevenDimDoors/mod_pocketDim/util/BaseConfigurationProcessor.java new file mode 100644 index 0000000..8fe75a2 --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/util/BaseConfigurationProcessor.java @@ -0,0 +1,52 @@ +package StevenDimDoors.mod_pocketDim.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +public abstract class BaseConfigurationProcessor +{ + public BaseConfigurationProcessor() { } + + public boolean canRead() + { + return true; + } + + public boolean canWrite() + { + return true; + } + + public T readFromFile(String path) throws FileNotFoundException, ConfigurationProcessingException + { + return readFromFile(new File(path)); + } + + public T readFromFile(File file) throws FileNotFoundException, ConfigurationProcessingException + { + return readFromStream(new FileInputStream(file)); + } + + public T readFromResource(String resourcePath) throws ConfigurationProcessingException + { + return readFromStream(this.getClass().getResourceAsStream(resourcePath)); + } + + public abstract T readFromStream(InputStream inputStream) throws ConfigurationProcessingException; + + public void writeToFile(File file, T data) throws FileNotFoundException, ConfigurationProcessingException + { + writeToStream(new FileOutputStream(file), data); + } + + public void writeToFile(String path, T data) throws FileNotFoundException, ConfigurationProcessingException + { + writeToFile(new File(path), data); + } + + public abstract void writeToStream(OutputStream outputStream, T data) throws ConfigurationProcessingException; +} diff --git a/StevenDimDoors/mod_pocketDim/util/ConfigurationProcessingException.java b/StevenDimDoors/mod_pocketDim/util/ConfigurationProcessingException.java new file mode 100644 index 0000000..1793f22 --- /dev/null +++ b/StevenDimDoors/mod_pocketDim/util/ConfigurationProcessingException.java @@ -0,0 +1,21 @@ +package StevenDimDoors.mod_pocketDim.util; + +public class ConfigurationProcessingException extends Exception +{ + private static final long serialVersionUID = -4525298050874891911L; + + public ConfigurationProcessingException() + { + super(); + } + + public ConfigurationProcessingException(String message) + { + super(message); + } + + public ConfigurationProcessingException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/StevenDimDoors/mod_pocketDim/util/WeightedContainer.java b/StevenDimDoors/mod_pocketDim/util/WeightedContainer.java index e69e48b..71332f3 100644 --- a/StevenDimDoors/mod_pocketDim/util/WeightedContainer.java +++ b/StevenDimDoors/mod_pocketDim/util/WeightedContainer.java @@ -17,11 +17,15 @@ public class WeightedContainer extends WeightedRandomItem { { super(weight); this.data = data; - super.itemWeight = weight; } public T getData() { return data; } + + public WeightedContainer clone() + { + return new WeightedContainer(data, itemWeight); + } } diff --git a/schematics/ruins/rules.txt b/schematics/ruins/rules.txt new file mode 100644 index 0000000..ef838c1 --- /dev/null +++ b/schematics/ruins/rules.txt @@ -0,0 +1,35 @@ +Version 1 +Types: +Hub +Trap +SimpleHall +ComplexHall +Exit +DeadEnd +Maze + +Settings: +AllowDuplicatesInChain = false +AllowPackChangeOut = true +DistortDoorCoordinates = true + +## Prevent this pack from being selected for transitioning in once we've transitioned out +AllowPackChangeIn = false + +Rules: + +Exit -> DeadEnd Exit + +DeadEnd -> DeadEnd Exit + +? ? ? ? ? ? ? ? -> Trap#20 SimpleHall#40 ComplexHall#10 Exit#20 DeadEnd#10 + +? ? ? ? -> Trap#18 SimpleHall#40 ComplexHall#10 Exit#18 DeadEnd#10 Hub#4 + +? ? ? -> ComplexHall Hub Trap SimpleHall Maze + +? ? -> ComplexHall Hub Trap SimpleHall Maze + +? -> ComplexHall#40 Hub#30 Trap#10 SimpleHall#10 Maze#10 + +-> ComplexHall#40 Hub#30 Trap#10 SimpleHall#10 Maze#10 \ No newline at end of file