diff --git a/core/src/main/java/tc/oc/pgm/snapshot/BudgetWorldEdit.java b/core/src/main/java/tc/oc/pgm/snapshot/BudgetWorldEdit.java index 73703d7f50..2c0d997472 100644 --- a/core/src/main/java/tc/oc/pgm/snapshot/BudgetWorldEdit.java +++ b/core/src/main/java/tc/oc/pgm/snapshot/BudgetWorldEdit.java @@ -15,15 +15,12 @@ */ class BudgetWorldEdit { - // Ugly optimization, match material via primitive id - private static final int AIR_ID = Material.AIR.getId(); + private final World world; + private final WorldSnapshot snapshot; - private final Match match; - private final SnapshotMatchModule smm; - - BudgetWorldEdit(Match match, SnapshotMatchModule snapshotMatchModule) { - this.match = match; - this.smm = snapshotMatchModule; + BudgetWorldEdit(World world, WorldSnapshot snapshot) { + this.world = world; + this.snapshot = snapshot; } /** @@ -31,12 +28,9 @@ class BudgetWorldEdit { * * @param region region where the blocks were when they got saved * @param offset the offset to add when placing blocks - * @param includeAir whether to place air if it's found in the memory */ - public void placeBlocks(Region region, BlockVector offset, boolean includeAir) { - final World world = match.getWorld(); - for (BlockData blockData : smm.getOriginalMaterials(region)) { - if (!includeAir && blockData.getTypeId() == AIR_ID) continue; + public void placeBlocks(Region region, BlockVector offset) { + for (BlockData blockData : snapshot.getMaterials(region)) { BlockState state = blockData.getBlock(world, offset).getState(); state.setMaterialData(blockData.getMaterialData()); @@ -49,14 +43,9 @@ public void placeBlocks(Region region, BlockVector offset, boolean includeAir) { * * @param region The region to remove blocks from * @param offset an offset to add to the region coordinates if the blocks were offset when placed - * @param includeAir if blocks that originally were air should be included or not */ - public void removeBlocks(Region region, BlockVector offset, boolean includeAir) { - final World world = match.getWorld(); - - for (BlockData blockData : smm.getOriginalMaterials(region)) { - if (!includeAir && blockData.getTypeId() == AIR_ID) continue; - + public void removeBlocks(Region region, BlockVector offset) { + for (BlockData blockData : snapshot.getMaterials(region)) { Block block = blockData.getBlock(world, offset); // Ignore if already air if (!block.getType().equals(Material.AIR)) block.setType(Material.AIR); diff --git a/core/src/main/java/tc/oc/pgm/snapshot/SnapshotMatchModule.java b/core/src/main/java/tc/oc/pgm/snapshot/SnapshotMatchModule.java index 3a3899a5e6..eeb935c89d 100644 --- a/core/src/main/java/tc/oc/pgm/snapshot/SnapshotMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/snapshot/SnapshotMatchModule.java @@ -1,16 +1,10 @@ package tc.oc.pgm.snapshot; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import org.bukkit.ChunkSnapshot; -import org.bukkit.Material; import org.bukkit.block.BlockState; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.material.MaterialData; -import org.bukkit.util.BlockVector; import org.bukkit.util.Vector; import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.event.BlockTransformEvent; @@ -19,11 +13,8 @@ import tc.oc.pgm.api.match.MatchScope; import tc.oc.pgm.api.match.factory.MatchModuleFactory; import tc.oc.pgm.api.module.exception.ModuleLoadException; -import tc.oc.pgm.api.region.Region; import tc.oc.pgm.events.ListenerScope; -import tc.oc.pgm.util.BlockData; import tc.oc.pgm.util.chunk.ChunkVector; -import tc.oc.pgm.util.nms.NMSHacks; /** * Keeps a snapshot of the block state of the entire match world at build time, using a @@ -45,66 +36,24 @@ public SnapshotMatchModule createMatchModule(Match match) throws ModuleLoadExcep } private final Match match; - private final Map chunkSnapshots = new HashMap<>(); - private final BudgetWorldEdit worldEdit; + // Represents the world state before any changes have been applied to it + private final WorldSnapshot snapshot; private SnapshotMatchModule(Match match) { this.match = match; - this.worldEdit = new BudgetWorldEdit(match, this); - } - - public MaterialData getOriginalMaterial(int x, int y, int z) { - if (y < 0 || y >= 256) return new MaterialData(Material.AIR); - - ChunkVector chunkVector = ChunkVector.ofBlock(x, y, z); - ChunkSnapshot chunkSnapshot = chunkSnapshots.get(chunkVector); - if (chunkSnapshot != null) { - BlockVector chunkPos = chunkVector.worldToChunk(x, y, z); - return new MaterialData( - chunkSnapshot.getBlockTypeId( - chunkPos.getBlockX(), chunkPos.getBlockY(), chunkPos.getBlockZ()), - (byte) - chunkSnapshot.getBlockData( - chunkPos.getBlockX(), chunkPos.getBlockY(), chunkPos.getBlockZ())); - } else { - return match.getWorld().getBlockAt(x, y, z).getState().getData(); - } + this.snapshot = new WorldSnapshot(match.getWorld()); } public MaterialData getOriginalMaterial(Vector pos) { - return getOriginalMaterial(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); + return snapshot.getOriginalMaterial(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); } public BlockState getOriginalBlock(int x, int y, int z) { - BlockState state = match.getWorld().getBlockAt(x, y, z).getState(); - if (y < 0 || y >= 256) return state; - - ChunkVector chunkVector = ChunkVector.ofBlock(x, y, z); - ChunkSnapshot chunkSnapshot = chunkSnapshots.get(chunkVector); - if (chunkSnapshot != null) { - BlockVector chunkPos = chunkVector.worldToChunk(x, y, z); - state.setMaterialData( - new MaterialData( - chunkSnapshot.getBlockTypeId( - chunkPos.getBlockX(), chunkPos.getBlockY(), chunkPos.getBlockZ()), - (byte) - chunkSnapshot.getBlockData( - chunkPos.getBlockX(), chunkPos.getBlockY(), chunkPos.getBlockZ()))); - } - return state; + return snapshot.getOriginalBlock(x, y, z); } public BlockState getOriginalBlock(Vector pos) { - return getOriginalBlock(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); - } - - /** - * Get the original material data for a {@code region}. - * - * @param region the region to get block states from - */ - public Iterable getOriginalMaterials(Region region) { - return () -> new BlockDataIterator(region); + return snapshot.getOriginalBlock(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ()); } // Listen on lowest priority so that the original block is available to other handlers of this @@ -122,95 +71,10 @@ public void onBlockChange(BlockTransformEvent event) { * @param state optional block state to write on the snapshot */ public void saveSnapshot(ChunkVector cv, @Nullable BlockState state) { - if (!chunkSnapshots.containsKey(cv)) { - this.match.getLogger().fine("Copying chunk at " + cv); - - ChunkSnapshot snapshot = cv.getChunk(match.getWorld()).getChunkSnapshot(); - - // ChunkSnapshot is very likely to have the post-event state already, - // so we have to correct it - if (state != null) NMSHacks.updateChunkSnapshot(snapshot, state); - - chunkSnapshots.put(cv, snapshot); - } - } - - public void saveRegion(Region region) { - region.getChunkPositions().forEach(cv -> this.saveSnapshot(cv, null)); - } - - public void placeBlocks(Region region, BlockVector offset, boolean includeAir) { - worldEdit.placeBlocks(region, offset, includeAir); - } - - public void removeBlocks(Region region, BlockVector offset, boolean includeAir) { - worldEdit.removeBlocks(region, offset, includeAir); + snapshot.saveSnapshot(cv, state); } - /** - * Works in a similar fashion to {@link tc.oc.pgm.util.block.CuboidBlockIterator}. Implements both - * {@link BlockData} and {@link Iterator}, changes its own state while iterating, and returns - * itself from {@link #next()}. In this way, it avoids creating any objects while iterating. It - * additionally provides no methods to mutate the state. - */ - private class BlockDataIterator implements Iterator, BlockData { - - private final Iterator vectors; - - private ChunkVector chunkVector = null; - private ChunkSnapshot snapshot = null; - - private BlockVector blockVector; - private int materialId; - private int data; - - private BlockDataIterator(Region region) { - this.vectors = region.getBlockVectorIterator(); - } - - @Override - public boolean hasNext() { - return this.vectors.hasNext(); - } - - @Override - public BlockData next() { - blockVector = this.vectors.next(); - - // If this block is in the same chunk as the previous one, keep using the same snapshot - // without fetching a new one - if (snapshot == null - || blockVector.getBlockZ() >> 4 != chunkVector.getChunkZ() - || blockVector.getBlockX() >> 4 != chunkVector.getChunkX()) { - chunkVector = ChunkVector.ofBlock(blockVector); - snapshot = chunkSnapshots.get(chunkVector); - } - - // Equivalent to chunkVector.worldToChunk(blockVector), but avoids allocations - int offsetX = blockVector.getBlockX() - chunkVector.getBlockMinX(); - int offsetY = blockVector.getBlockY(); - int offsetZ = blockVector.getBlockZ() - chunkVector.getBlockMinZ(); - - // Calling getMaterialData would cause an allocation, so instead use raw types - materialId = snapshot.getBlockTypeId(offsetX, offsetY, offsetZ); - data = snapshot.getBlockData(offsetX, offsetY, offsetZ); - - return this; - } - - @Override - public int getTypeId() { - return materialId; - } - - @Override - public int getData() { - return data; - } - - @Override - public BlockVector getBlockVector() { - return blockVector; - } + public WorldSnapshot getOriginalSnapshot() { + return snapshot; } } diff --git a/core/src/main/java/tc/oc/pgm/snapshot/WorldSnapshot.java b/core/src/main/java/tc/oc/pgm/snapshot/WorldSnapshot.java new file mode 100644 index 0000000000..a5cfa1f621 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/snapshot/WorldSnapshot.java @@ -0,0 +1,171 @@ +package tc.oc.pgm.snapshot; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import org.bukkit.ChunkSnapshot; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.BlockState; +import org.bukkit.material.MaterialData; +import org.bukkit.util.BlockVector; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.region.Region; +import tc.oc.pgm.util.BlockData; +import tc.oc.pgm.util.chunk.ChunkVector; +import tc.oc.pgm.util.nms.NMSHacks; + +public class WorldSnapshot { + + private final World world; + private final Map chunkSnapshots = new HashMap<>(); + private final BudgetWorldEdit worldEdit; + + public WorldSnapshot(World world) { + this.world = world; + this.worldEdit = new BudgetWorldEdit(world, this); + } + + public MaterialData getOriginalMaterial(int x, int y, int z) { + if (y < 0 || y >= 256) return new MaterialData(Material.AIR); + + ChunkVector chunkVector = ChunkVector.ofBlock(x, y, z); + ChunkSnapshot chunkSnapshot = chunkSnapshots.get(chunkVector); + if (chunkSnapshot != null) { + BlockVector chunkPos = chunkVector.worldToChunk(x, y, z); + return new MaterialData( + chunkSnapshot.getBlockTypeId( + chunkPos.getBlockX(), chunkPos.getBlockY(), chunkPos.getBlockZ()), + (byte) + chunkSnapshot.getBlockData( + chunkPos.getBlockX(), chunkPos.getBlockY(), chunkPos.getBlockZ())); + } else { + return world.getBlockAt(x, y, z).getState().getData(); + } + } + + public BlockState getOriginalBlock(int x, int y, int z) { + BlockState state = world.getBlockAt(x, y, z).getState(); + if (y < 0 || y >= 256) return state; + + ChunkVector chunkVector = ChunkVector.ofBlock(x, y, z); + ChunkSnapshot chunkSnapshot = chunkSnapshots.get(chunkVector); + if (chunkSnapshot != null) { + BlockVector chunkPos = chunkVector.worldToChunk(x, y, z); + state.setMaterialData( + new MaterialData( + chunkSnapshot.getBlockTypeId( + chunkPos.getBlockX(), chunkPos.getBlockY(), chunkPos.getBlockZ()), + (byte) + chunkSnapshot.getBlockData( + chunkPos.getBlockX(), chunkPos.getBlockY(), chunkPos.getBlockZ()))); + } + return state; + } + + /** + * Manually save the initial state of a block to the snapshot. + * + * @param cv the chunk vector to save + * @param state optional block state to write on the snapshot + */ + public void saveSnapshot(ChunkVector cv, @Nullable BlockState state) { + if (!chunkSnapshots.containsKey(cv)) { + ChunkSnapshot snapshot = cv.getChunk(world).getChunkSnapshot(); + + // ChunkSnapshot is very likely to have the post-event state already, + // so we have to correct it + if (state != null) NMSHacks.updateChunkSnapshot(snapshot, state); + + chunkSnapshots.put(cv, snapshot); + } + } + + public void saveRegion(Region region) { + region.getChunkPositions().forEach(cv -> this.saveSnapshot(cv, null)); + } + + public void placeBlocks(Region region, BlockVector offset) { + worldEdit.placeBlocks(region, offset); + } + + public void removeBlocks(Region region, BlockVector offset) { + worldEdit.removeBlocks(region, offset); + } + + /** + * Get the original material data for a {@code region}. + * + * @param region the region to get block states from + */ + public Iterable getMaterials(Region region) { + return () -> new BlockDataIterator(region); + } + + /** + * Works in a similar fashion to {@link tc.oc.pgm.util.block.CuboidBlockIterator}. Implements both + * {@link BlockData} and {@link Iterator}, changes its own state while iterating, and returns + * itself from {@link #next()}. In this way, it avoids creating any objects while iterating. It + * additionally provides no methods to mutate the state. + */ + private class BlockDataIterator implements Iterator, BlockData { + + private final Iterator vectors; + + private ChunkVector chunkVector = null; + private ChunkSnapshot snapshot = null; + + private BlockVector blockVector; + private int materialId; + private int data; + + private BlockDataIterator(Region region) { + this.vectors = region.getBlockVectorIterator(); + } + + @Override + public boolean hasNext() { + return this.vectors.hasNext(); + } + + @Override + public BlockData next() { + blockVector = this.vectors.next(); + + // If this block is in the same chunk as the previous one, keep using the same snapshot + // without fetching a new one + if (snapshot == null + || blockVector.getBlockZ() >> 4 != chunkVector.getChunkZ() + || blockVector.getBlockX() >> 4 != chunkVector.getChunkX()) { + chunkVector = ChunkVector.ofBlock(blockVector); + snapshot = chunkSnapshots.get(chunkVector); + } + + // Equivalent to chunkVector.worldToChunk(blockVector), but avoids allocations + int offsetX = blockVector.getBlockX() - chunkVector.getBlockMinX(); + int offsetY = blockVector.getBlockY(); + int offsetZ = blockVector.getBlockZ() - chunkVector.getBlockMinZ(); + + // Calling getMaterialData would cause an allocation, so instead use raw types + materialId = snapshot.getBlockTypeId(offsetX, offsetY, offsetZ); + data = snapshot.getBlockData(offsetX, offsetY, offsetZ); + + return this; + } + + @Override + public int getTypeId() { + return materialId; + } + + @Override + public int getData() { + return data; + } + + @Override + public BlockVector getBlockVector() { + return blockVector; + } + } +} diff --git a/core/src/main/java/tc/oc/pgm/structure/DynamicStructure.java b/core/src/main/java/tc/oc/pgm/structure/DynamicStructure.java index b8104369c6..67fa1ef4cf 100644 --- a/core/src/main/java/tc/oc/pgm/structure/DynamicStructure.java +++ b/core/src/main/java/tc/oc/pgm/structure/DynamicStructure.java @@ -1,31 +1,34 @@ package tc.oc.pgm.structure; import org.bukkit.util.BlockVector; -import org.bukkit.util.Vector; import tc.oc.pgm.api.feature.Feature; -import tc.oc.pgm.api.match.Match; -import tc.oc.pgm.snapshot.SnapshotMatchModule; +import tc.oc.pgm.api.region.Region; +import tc.oc.pgm.regions.TranslatedRegion; +import tc.oc.pgm.snapshot.WorldSnapshot; public class DynamicStructure implements Feature { - private final SnapshotMatchModule smm; - private final StructureDefinition structure; + private final Structure structure; private final BlockVector offset; + private final WorldSnapshot snapshot; private final DynamicStructureDefinition definition; + private final Region region; // Since the passive filter can skip placing the structure, // we need to keep track of whether its placed or not if we // want to avoid unnecessary clears. private boolean placed; - DynamicStructure(DynamicStructureDefinition definition, Match match) { - this.smm = match.needModule(SnapshotMatchModule.class); + DynamicStructure( + DynamicStructureDefinition definition, Structure structure, WorldSnapshot snapshot) { + this.structure = structure; this.definition = definition; - this.structure = this.definition.getStructureDefinition(); this.offset = this.definition.getOffset(); + this.snapshot = snapshot; + this.region = new TranslatedRegion(structure.getRegion(), offset); - // Position is the same as original structure, and it's not cleared - this.placed = offset.equals(new Vector()) && !structure.clearSource(); + // Save the blocks at original position before dynamic is placed + snapshot.saveRegion(region); } @Override @@ -42,14 +45,14 @@ public DynamicStructureDefinition getDefinition() { public void place() { if (placed) return; placed = true; - - smm.placeBlocks(structure.getRegion(), offset, structure.includeAir()); + structure.place(offset); } /** Remove the structure from the world */ public void clear() { if (!placed) return; placed = false; - smm.removeBlocks(structure.getRegion(), offset, structure.includeAir()); + + snapshot.placeBlocks(region, new BlockVector()); } } diff --git a/core/src/main/java/tc/oc/pgm/structure/Structure.java b/core/src/main/java/tc/oc/pgm/structure/Structure.java new file mode 100644 index 0000000000..d1ac5ec7df --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/structure/Structure.java @@ -0,0 +1,51 @@ +package tc.oc.pgm.structure; + +import org.bukkit.Material; +import org.bukkit.util.BlockVector; +import tc.oc.pgm.api.feature.Feature; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.region.Region; +import tc.oc.pgm.regions.FiniteBlockRegion; +import tc.oc.pgm.snapshot.WorldSnapshot; + +public class Structure implements Feature { + + private final StructureDefinition definition; + private final WorldSnapshot snapshot; + private final Region region; + + public Structure(StructureDefinition definition, Match match, WorldSnapshot snapshot) { + this.definition = definition; + this.snapshot = snapshot; + + if (definition.includeAir()) this.region = definition.getRegion(); + else + this.region = + FiniteBlockRegion.fromWorld( + definition.getRegion(), + match.getWorld(), + b -> b.getType() != Material.AIR, + match.getMap().getProto()); + + snapshot.saveRegion(region); + if (definition.clearSource()) snapshot.removeBlocks(region, new BlockVector()); + } + + @Override + public String getId() { + return getDefinition().getId(); + } + + @Override + public StructureDefinition getDefinition() { + return definition; + } + + public Region getRegion() { + return region; + } + + public void place(BlockVector offset) { + snapshot.placeBlocks(region, offset); + } +} diff --git a/core/src/main/java/tc/oc/pgm/structure/StructureDefinition.java b/core/src/main/java/tc/oc/pgm/structure/StructureDefinition.java index 9d95a799c0..c4f4916cb6 100644 --- a/core/src/main/java/tc/oc/pgm/structure/StructureDefinition.java +++ b/core/src/main/java/tc/oc/pgm/structure/StructureDefinition.java @@ -7,15 +7,19 @@ import tc.oc.pgm.api.feature.FeatureInfo; import tc.oc.pgm.api.region.Region; import tc.oc.pgm.features.SelfIdentifyingFeatureDefinition; +import tc.oc.pgm.regions.Bounds; @FeatureInfo(name = "structure") public class StructureDefinition extends SelfIdentifyingFeatureDefinition { private final Region region; - private Vector origin; // not final because of possible xml references private final boolean includeAir; private final boolean clearSource; + // Lazy init due to yet unresolved xml references + private Vector origin; + private Bounds bounds; + public StructureDefinition( String id, @Nullable Vector origin, Region region, boolean includeAir, boolean clearSource) { super(id); @@ -25,13 +29,6 @@ public StructureDefinition( this.clearSource = clearSource; } - public Vector getOrigin() { - if (origin == null) { - this.origin = region.getBounds().getMin(); - } - return this.origin; - } - public Region getRegion() { return region; } @@ -43,4 +40,18 @@ public boolean includeAir() { public boolean clearSource() { return clearSource; } + + public Vector getOrigin() { + if (origin == null) { + this.origin = getBounds().getMin(); + } + return this.origin; + } + + public Bounds getBounds() { + if (bounds == null) { + this.bounds = region.getBounds(); + } + return bounds; + } } diff --git a/core/src/main/java/tc/oc/pgm/structure/StructureMatchModule.java b/core/src/main/java/tc/oc/pgm/structure/StructureMatchModule.java index eddcb3dc1e..11c7bbf198 100644 --- a/core/src/main/java/tc/oc/pgm/structure/StructureMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/structure/StructureMatchModule.java @@ -5,13 +5,13 @@ import java.util.Map; import java.util.PriorityQueue; import java.util.Queue; -import org.bukkit.util.BlockVector; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.match.MatchModule; import tc.oc.pgm.api.match.MatchScope; import tc.oc.pgm.api.module.exception.ModuleLoadException; import tc.oc.pgm.filters.FilterMatchModule; import tc.oc.pgm.snapshot.SnapshotMatchModule; +import tc.oc.pgm.snapshot.WorldSnapshot; public class StructureMatchModule implements MatchModule { @@ -39,30 +39,41 @@ public StructureMatchModule( @Override public void load() throws ModuleLoadException { SnapshotMatchModule smm = match.needModule(SnapshotMatchModule.class); - for (StructureDefinition structure : structures.values()) { - smm.saveRegion(structure.getRegion()); + WorldSnapshot originalWorld = smm.getOriginalSnapshot(); - if (structure.clearSource()) - smm.removeBlocks(structure.getRegion(), new BlockVector(), structure.includeAir()); + boolean anyClears = false; + + for (StructureDefinition def : structures.values()) { + Structure structure = new Structure(def, match, originalWorld); + match.getFeatureContext().add(structure); + + anyClears |= def.clearSource(); } + // If no clears happened, then we're fine to use the same world snapshot. + // Otherwise, use a different one for dynamic clear + WorldSnapshot afterClear = anyClears ? new WorldSnapshot(match.getWorld()) : originalWorld; + FilterMatchModule fmm = match.needModule(FilterMatchModule.class); - for (DynamicStructureDefinition dynamicDefinition : dynamics) { - DynamicStructure dynamicStructure = new DynamicStructure(dynamicDefinition, match); + for (DynamicStructureDefinition def : dynamics) { + Structure struct = + match.getFeatureContext().get(def.getStructureDefinition().getId(), Structure.class); + + DynamicStructure dynamic = new DynamicStructure(def, struct, afterClear); + + match.getFeatureContext().add(dynamic); - match.getFeatureContext().add(dynamicStructure); - final DynamicStructureDefinition dynamicDef = dynamicStructure.getDefinition(); fmm.onChange( Match.class, - dynamicDef.getTrigger(), + dynamic.getDefinition().getTrigger(), (m, response) -> { if (response) { // This is the passive filter, not the dynamic one - if (dynamicDef.getPassive().query(m).isAllowed()) { - this.queuePlace(dynamicStructure); + if (dynamic.getDefinition().getPassive().query(m).isAllowed()) { + this.queuePlace(dynamic); } } else { - this.queueClear(dynamicStructure); + this.queueClear(dynamic); } }); } diff --git a/core/src/main/java/tc/oc/pgm/structure/StructureModule.java b/core/src/main/java/tc/oc/pgm/structure/StructureModule.java index c1d8942de9..7c574f42a3 100644 --- a/core/src/main/java/tc/oc/pgm/structure/StructureModule.java +++ b/core/src/main/java/tc/oc/pgm/structure/StructureModule.java @@ -95,7 +95,7 @@ public StructureModule parse(MapFactory factory, Logger logger, Document doc) "attributes 'location' and 'offset' cannot be used together", el); final StructureDefinition structure = - structures.get(el.getAttribute("structure").getValue()); + structures.get(XMLUtils.getRequiredAttribute(el, "structure").getValue()); Filter trigger, filter; if (proto.isOlderThan(MapProtos.DYNAMIC_FILTERS)) {