diff --git a/src/main/java/net/knarcraft/stargate/config/material/BukkitMaterialSpecifier.java b/src/main/java/net/knarcraft/stargate/config/material/BukkitMaterialSpecifier.java new file mode 100644 index 0000000..a761d5c --- /dev/null +++ b/src/main/java/net/knarcraft/stargate/config/material/BukkitMaterialSpecifier.java @@ -0,0 +1,36 @@ +package net.knarcraft.stargate.config.material; + +import org.bukkit.Material; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +/** + * A specifier for a Bukkit material + */ +public class BukkitMaterialSpecifier implements MaterialSpecifier { + + private final Material material; + + /** + * Instantiates a new material specifier + * + * @param material

The material to specify

+ */ + public BukkitMaterialSpecifier(@NotNull Material material) { + this.material = material; + } + + @Override + @NotNull + public String asString() { + return this.material.name(); + } + + @Override + @NotNull + public Set asMaterials() { + return Set.of(this.material); + } + +} diff --git a/src/main/java/net/knarcraft/stargate/config/material/BukkitTagSpecifier.java b/src/main/java/net/knarcraft/stargate/config/material/BukkitTagSpecifier.java new file mode 100644 index 0000000..8e4f6f4 --- /dev/null +++ b/src/main/java/net/knarcraft/stargate/config/material/BukkitTagSpecifier.java @@ -0,0 +1,35 @@ +package net.knarcraft.stargate.config.material; + +import org.bukkit.Material; +import org.bukkit.Tag; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +/** + * A specifier for a Bukkit material tag + */ +public class BukkitTagSpecifier implements MaterialSpecifier { + + private final Tag tag; + + /** + * Instantiates a new tag specifier + * + * @param tag

The tag to specify

+ */ + public BukkitTagSpecifier(@NotNull Tag tag) { + this.tag = tag; + } + + @Override + public @NotNull String asString() { + return "#" + this.tag.getKey().toString().replaceFirst("minecraft:", ""); + } + + @Override + public @NotNull Set asMaterials() { + return this.tag.getValues(); + } + +} diff --git a/src/main/java/net/knarcraft/stargate/config/material/MaterialSpecifier.java b/src/main/java/net/knarcraft/stargate/config/material/MaterialSpecifier.java new file mode 100644 index 0000000..9665c05 --- /dev/null +++ b/src/main/java/net/knarcraft/stargate/config/material/MaterialSpecifier.java @@ -0,0 +1,29 @@ +package net.knarcraft.stargate.config.material; + +import org.bukkit.Material; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +/** + * An interface describing a specifier for one or more Bukkit materials + */ +public interface MaterialSpecifier { + + /** + * Gets the string representation of the material specifier + * + *

This is used when saving the value to a gate file

+ */ + @NotNull + String asString(); + + /** + * Gets all the materials the specifier specifies + * + *

This is used when registering gate materials

+ */ + @NotNull + Set asMaterials(); + +} diff --git a/src/main/java/net/knarcraft/stargate/listener/BlockEventListener.java b/src/main/java/net/knarcraft/stargate/listener/BlockEventListener.java index 25230f8..78e0e37 100644 --- a/src/main/java/net/knarcraft/stargate/listener/BlockEventListener.java +++ b/src/main/java/net/knarcraft/stargate/listener/BlockEventListener.java @@ -2,6 +2,7 @@ package net.knarcraft.stargate.listener; import net.knarcraft.stargate.Stargate; import net.knarcraft.stargate.container.BlockChangeRequest; +import net.knarcraft.stargate.container.BlockLocation; import net.knarcraft.stargate.event.StargateDestroyEvent; import net.knarcraft.stargate.portal.Portal; import net.knarcraft.stargate.portal.PortalCreator; @@ -139,6 +140,13 @@ public class BlockEventListener implements Listener { boolean deny = false; String denyMessage = ""; + // Block breaking the button from breaking the entire Stargate + if (portal.getStructure().getButton() != null && portal.getStructure().getButton().equals( + new BlockLocation(event.getBlock()))) { + event.setCancelled(true); + return; + } + //Decide if the user can destroy the portal if (!PermissionHelper.canDestroyPortal(player, portal)) { denyMessage = Stargate.getString("denyMsg"); diff --git a/src/main/java/net/knarcraft/stargate/listener/PlayerEventListener.java b/src/main/java/net/knarcraft/stargate/listener/PlayerEventListener.java index 4720def..f75fc36 100644 --- a/src/main/java/net/knarcraft/stargate/listener/PlayerEventListener.java +++ b/src/main/java/net/knarcraft/stargate/listener/PlayerEventListener.java @@ -181,7 +181,8 @@ public class PlayerEventListener implements Listener { //Check an additional block away for BungeeCord portals using END_PORTAL as its material entrancePortal = PortalHandler.getByAdjacentEntrance(toLocation); if (entrancePortal == null || !entrancePortal.getOptions().isBungee() || - entrancePortal.getGate().getPortalOpenBlock() != Material.END_PORTAL) { + !MaterialHelper.specifiersToMaterials( + entrancePortal.getGate().getPortalOpenMaterials()).contains(Material.END_PORTAL)) { return false; } } diff --git a/src/main/java/net/knarcraft/stargate/portal/PortalActivator.java b/src/main/java/net/knarcraft/stargate/portal/PortalActivator.java index 55878f8..53f0ea0 100644 --- a/src/main/java/net/knarcraft/stargate/portal/PortalActivator.java +++ b/src/main/java/net/knarcraft/stargate/portal/PortalActivator.java @@ -3,6 +3,7 @@ package net.knarcraft.stargate.portal; import net.knarcraft.stargate.Stargate; import net.knarcraft.stargate.event.StargateActivateEvent; import net.knarcraft.stargate.event.StargateDeactivateEvent; +import net.knarcraft.stargate.utility.ListHelper; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -10,7 +11,6 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.Random; /** * The portal activator activates/de-activates portals and keeps track of a portal's destinations @@ -77,7 +77,7 @@ public class PortalActivator { return null; } //Get one random destination - String destination = destinations.get((new Random()).nextInt(destinations.size())); + String destination = ListHelper.getRandom(destinations); return PortalHandler.getByName(Portal.cleanString(destination), portalNetwork); } else { //Just return the normal fixed destination diff --git a/src/main/java/net/knarcraft/stargate/portal/PortalCreator.java b/src/main/java/net/knarcraft/stargate/portal/PortalCreator.java index b84489e..9668ba8 100644 --- a/src/main/java/net/knarcraft/stargate/portal/PortalCreator.java +++ b/src/main/java/net/knarcraft/stargate/portal/PortalCreator.java @@ -12,8 +12,11 @@ import net.knarcraft.stargate.portal.property.gate.Gate; import net.knarcraft.stargate.portal.property.gate.GateHandler; import net.knarcraft.stargate.utility.DirectionHelper; import net.knarcraft.stargate.utility.EconomyHelper; +import net.knarcraft.stargate.utility.ListHelper; +import net.knarcraft.stargate.utility.MaterialHelper; import net.knarcraft.stargate.utility.PermissionHelper; import net.knarcraft.stargate.utility.PortalFileHelper; +import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.Player; @@ -303,8 +306,12 @@ public class PortalCreator { } else { //Update the block type for the portal's opening to the closed block as the closed block can be anything, // not just air or water + @NotNull List possibleMaterials = MaterialHelper.specifiersToMaterials( + portal.getGate().getPortalClosedMaterials()).stream().toList(); + Material closedType = ListHelper.getRandom(possibleMaterials); + for (BlockLocation entrance : portal.getStructure().getEntrances()) { - entrance.setType(portal.getGate().getPortalClosedBlock()); + entrance.setType(closedType); } } } diff --git a/src/main/java/net/knarcraft/stargate/portal/PortalHandler.java b/src/main/java/net/knarcraft/stargate/portal/PortalHandler.java index caba7e3..cb0c029 100644 --- a/src/main/java/net/knarcraft/stargate/portal/PortalHandler.java +++ b/src/main/java/net/knarcraft/stargate/portal/PortalHandler.java @@ -8,6 +8,7 @@ import net.knarcraft.stargate.portal.property.PortalOption; import net.knarcraft.stargate.portal.property.PortalStructure; import net.knarcraft.stargate.portal.property.gate.Gate; import net.knarcraft.stargate.portal.property.gate.GateHandler; +import net.knarcraft.stargate.utility.MaterialHelper; import net.knarcraft.stargate.utility.PermissionHelper; import org.bukkit.Location; import org.bukkit.World; @@ -445,8 +446,9 @@ public class PortalHandler { for (RelativeBlockVector control : portal.getGate().getLayout().getControls()) { Block block = portal.getBlockAt(control).getBlock(); //Log control blocks not matching the gate layout - if (!block.getType().equals(portal.getGate().getControlBlock())) { - Stargate.debug("PortalHandler::destroyInvalidPortal", "Control Block Type == " + + if (!MaterialHelper.specifiersToMaterials(portal.getGate().getControlBlockMaterials()).contains( + block.getType())) { + Stargate.debug("PortalHandler::unregisterInvalidPortal", "Control Block Type == " + block.getType().name()); } } diff --git a/src/main/java/net/knarcraft/stargate/portal/PortalOpener.java b/src/main/java/net/knarcraft/stargate/portal/PortalOpener.java index 77d5dcb..ac41741 100644 --- a/src/main/java/net/knarcraft/stargate/portal/PortalOpener.java +++ b/src/main/java/net/knarcraft/stargate/portal/PortalOpener.java @@ -6,6 +6,8 @@ import net.knarcraft.stargate.container.BlockLocation; import net.knarcraft.stargate.event.StargateCloseEvent; import net.knarcraft.stargate.event.StargateOpenEvent; import net.knarcraft.stargate.portal.property.PortalOptions; +import net.knarcraft.stargate.utility.ListHelper; +import net.knarcraft.stargate.utility.MaterialHelper; import org.bukkit.Axis; import org.bukkit.Material; import org.bukkit.block.data.Orientable; @@ -13,6 +15,8 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.List; + /** * The portal opener is responsible for opening and closing a portal */ @@ -87,7 +91,10 @@ public class PortalOpener { } //Get the material to change the opening to - Material openType = portal.getGate().getPortalOpenBlock(); + @NotNull List possibleMaterials = MaterialHelper.specifiersToMaterials( + portal.getGate().getPortalOpenMaterials()).stream().toList(); + Material openType = ListHelper.getRandom(possibleMaterials); + //Adjust orientation if applicable Axis axis = (openType.createBlockData() instanceof Orientable) ? portal.getLocation().getRotationAxis() : null; @@ -168,9 +175,15 @@ public class PortalOpener { } //Close the portal by requesting the opening blocks to change - Material closedType = portal.getGate().getPortalClosedBlock(); + @NotNull List possibleMaterials = MaterialHelper.specifiersToMaterials( + portal.getGate().getPortalClosedMaterials()).stream().toList(); + Material closedType = ListHelper.getRandom(possibleMaterials); + + //Adjust orientation if applicable + Axis axis = (closedType.createBlockData() instanceof Orientable) ? portal.getLocation().getRotationAxis() : null; + for (BlockLocation entrance : portal.getStructure().getEntrances()) { - Stargate.addBlockChangeRequest(new BlockChangeRequest(entrance, closedType, null)); + Stargate.addBlockChangeRequest(new BlockChangeRequest(entrance, closedType, axis)); } //Update the portal state to make it actually closed diff --git a/src/main/java/net/knarcraft/stargate/portal/property/gate/Gate.java b/src/main/java/net/knarcraft/stargate/portal/property/gate/Gate.java index 57c6dd2..efcfc30 100644 --- a/src/main/java/net/knarcraft/stargate/portal/property/gate/Gate.java +++ b/src/main/java/net/knarcraft/stargate/portal/property/gate/Gate.java @@ -1,10 +1,12 @@ package net.knarcraft.stargate.portal.property.gate; import net.knarcraft.stargate.Stargate; +import net.knarcraft.stargate.config.material.BukkitMaterialSpecifier; +import net.knarcraft.stargate.config.material.MaterialSpecifier; import net.knarcraft.stargate.container.BlockLocation; import net.knarcraft.stargate.container.RelativeBlockVector; +import net.knarcraft.stargate.utility.MaterialHelper; import org.bukkit.Material; -import org.bukkit.Tag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -13,6 +15,7 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -24,13 +27,12 @@ public class Gate { private final String filename; private final GateLayout layout; - private final Map characterMaterialMap; - private final Map> characterTagMap; + private final Map> characterMaterialMap; //Gate materials - private final Material portalOpenBlock; - private final Material portalClosedBlock; - private final Material portalButton; + private final List portalOpenMaterials; + private final List portalClosedMaterials; + private final List portalButtonMaterials; //Economy information private final int useCost; @@ -41,34 +43,33 @@ public class Gate { /** * Instantiates a new gate * - * @param filename

The name of the gate file, including extension

- * @param layout

The gate layout defined in the gate file

- * @param characterMaterialMap

The material types the different layout characters represent

- * @param characterTagMap

The material tag types the different layout characters represent

- * @param portalOpenBlock

The material to set the opening to when the portal is open

- * @param portalClosedBlock

The material to set the opening to when the portal is closed

- * @param portalButton

The material to use for the portal button

- * @param useCost

The cost of using a portal with this gate layout (-1 to disable)

- * @param createCost

The cost of creating a portal with this gate layout (-1 to disable)

- * @param destroyCost

The cost of destroying a portal with this gate layout (-1 to disable)

- * @param toOwner

Whether any payment should go to the owner of the gate, as opposed to just disappearing

+ * @param filename

The name of the gate file, including extension

+ * @param layout

The gate layout defined in the gate file

+ * @param characterMaterialsMap

The material types the different layout characters represent

+ * @param portalOpenMaterials

The material to set the opening to when the portal is open

+ * @param portalClosedMaterials

The material to set the opening to when the portal is closed

+ * @param portalButtonMaterials

The material to use for the portal button

+ * @param useCost

The cost of using a portal with this gate layout (-1 to disable)

+ * @param createCost

The cost of creating a portal with this gate layout (-1 to disable)

+ * @param destroyCost

The cost of destroying a portal with this gate layout (-1 to disable)

+ * @param toOwner

Whether any payment should go to the owner of the gate, as opposed to just disappearing

*/ public Gate(@NotNull String filename, @NotNull GateLayout layout, - @NotNull Map characterMaterialMap, - @NotNull Map> characterTagMap, @NotNull Material portalOpenBlock, - @NotNull Material portalClosedBlock, @NotNull Material portalButton, int useCost, int createCost, - int destroyCost, boolean toOwner) { + @NotNull Map> characterMaterialsMap, + @NotNull List portalOpenMaterials, + @NotNull List portalClosedMaterials, + @NotNull List portalButtonMaterials, int useCost, int createCost, int destroyCost, + boolean toOwner) { this.filename = filename; this.layout = layout; - this.characterMaterialMap = characterMaterialMap; - this.portalOpenBlock = portalOpenBlock; - this.portalClosedBlock = portalClosedBlock; - this.portalButton = portalButton; + this.characterMaterialMap = characterMaterialsMap; + this.portalOpenMaterials = portalOpenMaterials; + this.portalClosedMaterials = portalClosedMaterials; + this.portalButtonMaterials = portalButtonMaterials; this.useCost = useCost; this.createCost = createCost; this.destroyCost = destroyCost; this.toOwner = toOwner; - this.characterTagMap = characterTagMap; } /** @@ -87,7 +88,7 @@ public class Gate { * @return

The character to material map

*/ @NotNull - public Map getCharacterMaterialMap() { + public Map> getCharacterMaterialMap() { return new HashMap<>(characterMaterialMap); } @@ -98,18 +99,7 @@ public class Gate { * @return

True if the material is valid for control blocks

*/ public boolean isValidControlBlock(@NotNull Material material) { - return (getControlBlock() != null) ? getControlBlock().equals(material) : - getControlBlockTag().isTagged(material); - } - - /** - * Gets the material tag used for this gate's control blocks - * - * @return

The material tag type used for control blocks

- */ - @NotNull - public Tag getControlBlockTag() { - return characterTagMap.get(GateHandler.getControlBlockCharacter()); + return getControlBlockMaterials().contains(new BukkitMaterialSpecifier(material)); } /** @@ -117,8 +107,8 @@ public class Gate { * * @return

The material type used for control blocks

*/ - @Nullable - public Material getControlBlock() { + @NotNull + public List getControlBlockMaterials() { return characterMaterialMap.get(GateHandler.getControlBlockCharacter()); } @@ -138,8 +128,8 @@ public class Gate { * @return

The block type to use for the opening when open

*/ @NotNull - public Material getPortalOpenBlock() { - return portalOpenBlock; + public List getPortalOpenMaterials() { + return portalOpenMaterials; } /** @@ -148,8 +138,8 @@ public class Gate { * @return

The block type to use for the opening when closed

*/ @NotNull - public Material getPortalClosedBlock() { - return portalClosedBlock; + public List getPortalClosedMaterials() { + return portalClosedMaterials; } /** @@ -158,8 +148,8 @@ public class Gate { * @return

The material to use for a portal's button if using this gate type

*/ @NotNull - public Material getPortalButton() { - return portalButton; + public List getPortalButtonMaterials() { + return portalButtonMaterials; } /** @@ -236,35 +226,27 @@ public class Gate { * @return

True if all border blocks of the gate match the layout

*/ private boolean verifyGateBorderMatches(@NotNull BlockLocation topLeft, double yaw) { - Map characterMaterialMap = new HashMap<>(this.characterMaterialMap); - Map> characterTagMap = new HashMap<>(this.characterTagMap); + Map> characterMaterialMap = new HashMap<>(this.characterMaterialMap); for (RelativeBlockVector borderVector : layout.getBorder()) { int rowIndex = borderVector.right(); int lineIndex = borderVector.down(); Character key = layout.getLayout()[lineIndex][rowIndex]; - Material materialInLayout = characterMaterialMap.get(key); - Tag tagInLayout = characterTagMap.get(key); + List materialInLayout = characterMaterialMap.get(key); Material materialAtLocation = topLeft.getRelativeLocation(borderVector, yaw).getType(); if (materialInLayout != null) { - if (materialAtLocation != materialInLayout) { + if (!MaterialHelper.specifiersToMaterials(materialInLayout).contains(materialAtLocation)) { Stargate.debug("Gate::Matches", String.format("Block Type Mismatch: %s != %s", materialAtLocation, materialInLayout)); return false; } - } else if (tagInLayout != null) { - if (!tagInLayout.isTagged(materialAtLocation)) { - Stargate.debug("Gate::Matches", String.format("Block Type Mismatch: %s != %s", - materialAtLocation, tagInLayout)); - return false; - } } else { /* This generally should not happen with proper checking, but just in case a material character is not * recognized, but still allowed in previous checks, verify the gate as long as all such instances of * the character correspond to the same material in the physical gate. All subsequent gates will also * need to match the first verified gate. */ - characterMaterialMap.put(key, materialAtLocation); + characterMaterialMap.put(key, List.of(new BukkitMaterialSpecifier(materialAtLocation))); Stargate.debug("Gate::Matches", String.format("Missing layout material in %s. Using %s from the" + " physical portal.", getFilename(), materialAtLocation)); } @@ -291,7 +273,8 @@ public class Gate { continue; } - if (type != portalClosedBlock && type != portalOpenBlock) { + if (!MaterialHelper.specifiersToMaterials(portalClosedMaterials).contains(type) && + !MaterialHelper.specifiersToMaterials(portalOpenMaterials).contains(type)) { Stargate.debug("Gate::Matches", "Entrance/Exit Material Mismatch: " + type); return false; } @@ -311,9 +294,9 @@ public class Gate { BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(new File(gateFolder, filename))); //Save main material names - writeConfig(bufferedWriter, "portal-open", portalOpenBlock.name()); - writeConfig(bufferedWriter, "portal-closed", portalClosedBlock.name()); - writeConfig(bufferedWriter, "button", portalButton.name()); + writeConfig(bufferedWriter, "portal-open", MaterialHelper.specifiersToString(portalOpenMaterials)); + writeConfig(bufferedWriter, "portal-closed", MaterialHelper.specifiersToString(portalClosedMaterials)); + writeConfig(bufferedWriter, "button", MaterialHelper.specifiersToString(portalButtonMaterials)); //Save the values necessary for economy saveEconomyValues(bufferedWriter); @@ -361,7 +344,7 @@ public class Gate { * @throws IOException

If unable to write to the buffered writer

*/ private void saveFrameBlockType(@NotNull BufferedWriter bufferedWriter) throws IOException { - for (Map.Entry entry : this.characterMaterialMap.entrySet()) { + for (Map.Entry> entry : this.characterMaterialMap.entrySet()) { Character key = entry.getKey(); //Skip characters not part of the frame if (key.equals(GateHandler.getAnythingCharacter()) || @@ -369,11 +352,7 @@ public class Gate { key.equals(GateHandler.getExitCharacter())) { continue; } - saveFrameBlockType(key, entry.getValue().toString(), bufferedWriter); - } - for (Map.Entry> entry : this.characterTagMap.entrySet()) { - saveFrameBlockType(entry.getKey(), "#" + entry.getValue().getKey().toString().replaceFirst( - "minecraft:", ""), bufferedWriter); + saveFrameBlockType(key, MaterialHelper.specifiersToString(entry.getValue()), bufferedWriter); } } diff --git a/src/main/java/net/knarcraft/stargate/portal/property/gate/GateHandler.java b/src/main/java/net/knarcraft/stargate/portal/property/gate/GateHandler.java index ebdce4e..7a26ef4 100644 --- a/src/main/java/net/knarcraft/stargate/portal/property/gate/GateHandler.java +++ b/src/main/java/net/knarcraft/stargate/portal/property/gate/GateHandler.java @@ -1,7 +1,8 @@ package net.knarcraft.stargate.portal.property.gate; import net.knarcraft.stargate.Stargate; -import net.knarcraft.stargate.utility.GateReader; +import net.knarcraft.stargate.config.material.BukkitMaterialSpecifier; +import net.knarcraft.stargate.config.material.MaterialSpecifier; import net.knarcraft.stargate.utility.MaterialHelper; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -18,6 +19,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Scanner; +import java.util.Set; +import java.util.function.Predicate; import static net.knarcraft.stargate.utility.GateReader.generateLayoutMatrix; import static net.knarcraft.stargate.utility.GateReader.readGateConfig; @@ -94,20 +97,13 @@ public class GateHandler { private static void registerGate(@NotNull Gate gate) { gates.put(gate.getFilename(), gate); - Material blockId = gate.getControlBlock(); - if (blockId != null) { - if (!controlBlocks.containsKey(blockId)) { - controlBlocks.put(blockId, new ArrayList<>()); + Set blockTypes = MaterialHelper.specifiersToMaterials(gate.getControlBlockMaterials()); + for (Material material : blockTypes) { + if (!controlBlocks.containsKey(material)) { + controlBlocks.put(material, new ArrayList<>()); } - controlBlocks.get(blockId).add(gate); - return; + controlBlocks.get(material).add(gate); } - - Tag materialTag = gate.getControlBlockTag(); - if (!controlBlockTags.containsKey(materialTag.getKey().toString())) { - controlBlockTags.put(materialTag.getKey().toString(), new ArrayList<>()); - } - controlBlockTags.get(materialTag.getKey().toString()).add(gate); } /** @@ -138,24 +134,23 @@ public class GateHandler { private static Gate loadGate(@NotNull String fileName, @NotNull String parentFolder, @NotNull Scanner scanner) { List> design = new ArrayList<>(); - Map characterMaterialMap = new HashMap<>(); - Map> characterTagMap = new HashMap<>(); + Map> characterMaterialMap = new HashMap<>(); Map config = new HashMap<>(); //Initialize character to material map - characterMaterialMap.put(ENTRANCE, Material.AIR); - characterMaterialMap.put(EXIT, Material.AIR); - characterMaterialMap.put(ANYTHING, Material.AIR); + characterMaterialMap.put(ENTRANCE, List.of(new BukkitMaterialSpecifier(Material.AIR))); + characterMaterialMap.put(EXIT, List.of(new BukkitMaterialSpecifier(Material.AIR))); + characterMaterialMap.put(ANYTHING, List.of(new BukkitMaterialSpecifier(Material.AIR))); //Read the file into appropriate lists and maps - int columns = readGateFile(scanner, characterMaterialMap, characterTagMap, fileName, design, config); + int columns = readGateFile(scanner, characterMaterialMap, fileName, design, config); if (columns < 0) { return null; } Character[][] layout = generateLayoutMatrix(design, columns); //Create and validate the new gate - Gate gate = createGate(config, fileName, layout, characterMaterialMap, characterTagMap); + Gate gate = createGate(config, fileName, layout, characterMaterialMap); if (gate == null) { return null; } @@ -172,28 +167,26 @@ public class GateHandler { * @param fileName

The name of the saved gate config file

* @param layout

The layout matrix of the new gate

* @param characterMaterialMap

A map between layout characters and the material to use

- * @param materialTagMap

A map between layout characters and the material tags to use

* @return

A new gate, or null if the config is invalid

*/ @Nullable private static Gate createGate(@NotNull Map config, @NotNull String fileName, @NotNull Character[][] layout, - @NotNull Map characterMaterialMap, - @NotNull Map> materialTagMap) { + @NotNull Map> characterMaterialMap) { //Read relevant material types - Material portalOpenBlock = readGateConfig(config, fileName, "portal-open", defaultPortalBlockOpen); - Material portalClosedBlock = readGateConfig(config, fileName, "portal-closed", defaultPortalBlockClosed); - Material portalButton = readGateConfig(config, fileName, "button", defaultButton); + List portalOpenBlock = readGateConfig(config, fileName, "portal-open", defaultPortalBlockOpen); + List portalClosedBlock = readGateConfig(config, fileName, "portal-closed", defaultPortalBlockClosed); + List portalButton = readGateConfig(config, fileName, "button", defaultButton); //Read economy values - int useCost = GateReader.readGateConfig(config, fileName, "usecost"); - int createCost = GateReader.readGateConfig(config, fileName, "createcost"); - int destroyCost = GateReader.readGateConfig(config, fileName, "destroycost"); + int useCost = readGateConfig(config, fileName, "usecost"); + int createCost = readGateConfig(config, fileName, "createcost"); + int destroyCost = readGateConfig(config, fileName, "destroycost"); boolean toOwner = (config.containsKey("toowner") ? Boolean.parseBoolean(config.get("toowner")) : Stargate.getEconomyConfig().sendPaymentToOwner()); //Create the new gate - Gate gate = new Gate(fileName, new GateLayout(layout), characterMaterialMap, materialTagMap, portalOpenBlock, + Gate gate = new Gate(fileName, new GateLayout(layout), characterMaterialMap, portalOpenBlock, portalClosedBlock, portalButton, useCost, createCost, destroyCost, toOwner); if (!validateGate(gate, fileName)) { @@ -217,23 +210,23 @@ public class GateHandler { return false; } - if (!MaterialHelper.isButtonCompatible(gate.getPortalButton())) { + if (checkMaterialPredicateFail(gate.getPortalButtonMaterials(), MaterialHelper::isButtonCompatible)) { Stargate.logSevere(String.format(failString, "Gate button must be a type of button.")); return false; } - if (!gate.getPortalOpenBlock().isBlock()) { + if (checkMaterialPredicateFail(gate.getPortalOpenMaterials(), Material::isBlock)) { Stargate.logSevere(String.format(failString, "Gate open block must be a type of block.")); return false; } - if (!gate.getPortalClosedBlock().isBlock()) { + if (checkMaterialPredicateFail(gate.getPortalClosedMaterials(), Material::isBlock)) { Stargate.logSevere(String.format(failString, "Gate closed block must be a type of block.")); return false; } - for (Material material : gate.getCharacterMaterialMap().values()) { - if (!material.isBlock()) { + for (List materialSpecifiers : gate.getCharacterMaterialMap().values()) { + if (checkMaterialPredicateFail(materialSpecifiers, Material::isBlock)) { Stargate.logSevere(String.format(failString, "Every gate border block must be a type of block.")); return false; } @@ -242,6 +235,25 @@ public class GateHandler { return true; } + /** + * Checks whether a predicate is true for a list of material specifiers + * + * @param materialSpecifiers

The material specifiers to test

+ * @param predicate

The predicate to test

+ * @return

True if the predicate failed for any specified materials

+ */ + private static boolean checkMaterialPredicateFail(@NotNull List materialSpecifiers, + @NotNull Predicate predicate) { + Set closedMaterials = MaterialHelper.specifiersToMaterials(materialSpecifiers); + for (Material material : closedMaterials) { + if (!predicate.test(material)) { + return true; + } + } + + return false; + } + /** * Loads all gates inside the given folder * diff --git a/src/main/java/net/knarcraft/stargate/utility/GateReader.java b/src/main/java/net/knarcraft/stargate/utility/GateReader.java index d8804fb..f57c6c0 100644 --- a/src/main/java/net/knarcraft/stargate/utility/GateReader.java +++ b/src/main/java/net/knarcraft/stargate/utility/GateReader.java @@ -1,10 +1,9 @@ package net.knarcraft.stargate.utility; import net.knarcraft.stargate.Stargate; -import org.bukkit.Bukkit; +import net.knarcraft.stargate.config.material.BukkitMaterialSpecifier; +import net.knarcraft.stargate.config.material.MaterialSpecifier; import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.Tag; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -26,15 +25,15 @@ public final class GateReader { * * @param scanner

The scanner to read from

* @param characterMaterialMap

The map of characters to store valid symbols in

- * @param materialTagMap

The map of characters to store valid tag symbols in

* @param fileName

The filename of the loaded gate config file

* @param design

The list to store the loaded design/layout to

* @param config

The map of config values to store to

* @return

The column count/width of the loaded gate

*/ - public static int readGateFile(@NotNull Scanner scanner, @NotNull Map characterMaterialMap, - @NotNull Map> materialTagMap, @NotNull String fileName, - @NotNull List> design, Map config) { + public static int readGateFile(@NotNull Scanner scanner, + @NotNull Map> characterMaterialMap, + @NotNull String fileName, @NotNull List> design, + @NotNull Map config) { boolean designing = false; int columns = 0; try (scanner) { @@ -43,14 +42,14 @@ public final class GateReader { if (designing) { //If we have reached the gate's layout/design, read it - columns = readGateDesignLine(line, columns, characterMaterialMap, materialTagMap, fileName, design); + columns = readGateDesignLine(line, columns, characterMaterialMap, fileName, design); if (columns < 0) { return -1; } } else { if (!line.isEmpty() && !line.startsWith("#")) { //Read a normal config value - readGateConfigValue(line, characterMaterialMap, materialTagMap, config); + readGateConfigValue(line, characterMaterialMap, config); } else if ((line.isEmpty()) || (!line.contains("=") && !line.startsWith("#"))) { //An empty line marks the start of the gate's layout/design designing = true; @@ -73,14 +72,12 @@ public final class GateReader { * @param line

The line to read

* @param maxColumns

The current max columns value of the design

* @param characterMaterialMap

The map between characters and the corresponding materials to use

- * @param materialTagMap

The map between characters and the corresponding material tags to use

* @param fileName

The filename of the loaded gate config file

* @param design

The two-dimensional list to store the loaded design to

* @return

The new max columns value of the design

*/ private static int readGateDesignLine(@NotNull String line, int maxColumns, - @NotNull Map characterMaterialMap, - @NotNull Map> materialTagMap, + @NotNull Map> characterMaterialMap, @NotNull String fileName, @NotNull List> design) { List row = new ArrayList<>(); @@ -91,7 +88,7 @@ public final class GateReader { for (Character symbol : line.toCharArray()) { //Refuse read gate designs with unknown characters - if (symbol.equals('?') || (!characterMaterialMap.containsKey(symbol) && !materialTagMap.containsKey(symbol))) { + if (symbol.equals('?') || !characterMaterialMap.containsKey(symbol)) { Stargate.logSevere(String.format("Could not load Gate %s - Unknown symbol '%s' in diagram", fileName, symbol)); return -1; @@ -110,12 +107,11 @@ public final class GateReader { * * @param line

The line to read

* @param characterMaterialMap

The character to material map to store to

- * @param materialTagMap

The character to material tag map to store to

* @param config

The config value map to store to

* @throws Exception

If an invalid material is encountered

*/ - private static void readGateConfigValue(@NotNull String line, @NotNull Map characterMaterialMap, - @NotNull Map> materialTagMap, + private static void readGateConfigValue(@NotNull String line, + @NotNull Map> characterMaterialMap, @NotNull Map config) throws Exception { String[] split = line.split("="); String key = split[0].trim(); @@ -125,23 +121,12 @@ public final class GateReader { //Read a gate frame material Character symbol = key.charAt(0); - if (value.startsWith("#")) { - String tagString = value.replaceFirst("#", ""); - Tag tag = Bukkit.getTag(Tag.REGISTRY_BLOCKS, NamespacedKey.minecraft(tagString.toLowerCase()), - Material.class); - if (tag != null) { - materialTagMap.put(symbol, tag); - return; - } + List materials = MaterialHelper.parseTagsAndMaterials(value); + if (!materials.isEmpty()) { + characterMaterialMap.put(symbol, materials); } else { - Material material = Material.matchMaterial(value); - if (material != null) { - //Register the map between the read symbol and the corresponding material - characterMaterialMap.put(symbol, material); - return; - } + throw new Exception("Invalid material in line: " + line); } - throw new Exception("Invalid material in line: " + line); } else { //Read a normal config value config.put(key, value); @@ -180,17 +165,17 @@ public final class GateReader { * @return

The material specified in the config, or the default material if it could not be read

*/ @NotNull - public static Material readGateConfig(@NotNull Map config, @NotNull String fileName, - @NotNull String key, @NotNull Material defaultMaterial) { + public static List readGateConfig(@NotNull Map config, @NotNull String fileName, + @NotNull String key, @NotNull Material defaultMaterial) { if (config.containsKey(key)) { - Material material = Material.matchMaterial(config.get(key)); - if (material != null) { - return material; + List materialSpecifiers = MaterialHelper.parseTagsAndMaterials(config.get(key)); + if (!materialSpecifiers.isEmpty()) { + return materialSpecifiers; } else { Stargate.logWarning(String.format("Error reading %s: %s is not a material", fileName, key)); } } - return defaultMaterial; + return List.of(new BukkitMaterialSpecifier(defaultMaterial)); } /** diff --git a/src/main/java/net/knarcraft/stargate/utility/ListHelper.java b/src/main/java/net/knarcraft/stargate/utility/ListHelper.java new file mode 100644 index 0000000..fa82656 --- /dev/null +++ b/src/main/java/net/knarcraft/stargate/utility/ListHelper.java @@ -0,0 +1,28 @@ +package net.knarcraft.stargate.utility; + +import java.util.List; +import java.util.Random; + +/** + * A helper class for dealing with lists + */ +public final class ListHelper { + + private static final Random random = new Random(); + + private ListHelper() { + + } + + /** + * Gets a random item from a list + * + * @param list

The list to get an item from

+ * @param

The type of item the list contains

+ * @return

A random item

+ */ + public static T getRandom(List list) { + return list.get(random.nextInt(list.size())); + } + +} diff --git a/src/main/java/net/knarcraft/stargate/utility/MaterialHelper.java b/src/main/java/net/knarcraft/stargate/utility/MaterialHelper.java index 466e844..2e310bc 100644 --- a/src/main/java/net/knarcraft/stargate/utility/MaterialHelper.java +++ b/src/main/java/net/knarcraft/stargate/utility/MaterialHelper.java @@ -1,8 +1,19 @@ package net.knarcraft.stargate.utility; +import net.knarcraft.stargate.config.material.BukkitMaterialSpecifier; +import net.knarcraft.stargate.config.material.BukkitTagSpecifier; +import net.knarcraft.stargate.config.material.MaterialSpecifier; +import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.Tag; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** * This class helps decide properties of materials not already present in the Spigot API @@ -50,4 +61,80 @@ public final class MaterialHelper { return Tag.BUTTONS.isTagged(material) || isWallCoral(material) || isContainer(material); } + @NotNull + public static String specifiersToString(@NotNull List specifiers) { + List names = new ArrayList<>(); + for (MaterialSpecifier specifier : specifiers) { + names.add(specifier.asString()); + } + return String.join(",", names); + } + + /** + * Converts a list of material specifiers to a set of materials + * + * @param specifiers

The material specifiers to convert

+ * @return

The materials the specifiers represent

+ */ + @NotNull + public static Set specifiersToMaterials(@NotNull List specifiers) { + Set output = new HashSet<>(); + + for (MaterialSpecifier specifier : specifiers) { + output.addAll(specifier.asMaterials()); + } + + return output; + } + + /** + * Parses all materials and material tags found in the input string + * + * @param input

The input string to parse

+ * @return

All material specifiers found

+ */ + @NotNull + public static List parseTagsAndMaterials(@NotNull String input) { + List specifiers = new ArrayList<>(); + + // Nothing to parse + if (input.isBlank()) { + return specifiers; + } + + String[] parts; + if (input.contains(",")) { + parts = input.split(","); + } else { + parts = new String[]{input}; + } + + for (String part : parts) { + MaterialSpecifier materialSpecifier = parseTagOrMaterial(part.trim()); + if (materialSpecifier != null) { + specifiers.add(materialSpecifier); + } + } + + return specifiers; + } + + @Nullable + private static MaterialSpecifier parseTagOrMaterial(@NotNull String input) { + if (input.startsWith("#")) { + String tagString = input.replaceFirst("#", "").toLowerCase(); + Tag tag = Bukkit.getTag(Tag.REGISTRY_BLOCKS, NamespacedKey.minecraft(tagString), Material.class); + if (tag != null) { + return new BukkitTagSpecifier(tag); + } + } else { + Material material = Material.matchMaterial(input); + if (material != null) { + return new BukkitMaterialSpecifier(material); + } + } + + return null; + } + } diff --git a/src/main/java/net/knarcraft/stargate/utility/PermissionHelper.java b/src/main/java/net/knarcraft/stargate/utility/PermissionHelper.java index 2416e98..dcef0e0 100644 --- a/src/main/java/net/knarcraft/stargate/utility/PermissionHelper.java +++ b/src/main/java/net/knarcraft/stargate/utility/PermissionHelper.java @@ -409,11 +409,13 @@ public final class PermissionHelper { } //Player cannot access portal - if (!PermissionHelper.cannotAccessPortal(player, entrancePortal, destination)) { + if (PermissionHelper.cannotAccessPortal(player, entrancePortal, destination)) { if (!entrancePortal.getOptions().isSilent()) { Stargate.getMessageSender().sendErrorMessage(player, Stargate.getString("denyMsg")); } new PlayerTeleporter(entrancePortal, player).teleportPlayer(entrancePortal, event); + Stargate.debug("PermissionHelper::playerCannotTeleport", "Closed portal because player is " + + "missing necessary permissions"); entrancePortal.getPortalOpener().closePortal(false); return true; } diff --git a/src/main/java/net/knarcraft/stargate/utility/PortalFileHelper.java b/src/main/java/net/knarcraft/stargate/utility/PortalFileHelper.java index 33ca2d5..98ff915 100644 --- a/src/main/java/net/knarcraft/stargate/utility/PortalFileHelper.java +++ b/src/main/java/net/knarcraft/stargate/utility/PortalFileHelper.java @@ -27,6 +27,7 @@ import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.util.List; import java.util.Scanner; import static net.knarcraft.stargate.portal.PortalSignDrawer.markPortalWithInvalidGate; @@ -399,9 +400,15 @@ public final class PortalFileHelper { return; } - Directional buttonData = (Directional) Bukkit.createBlockData(portal.getGate().getPortalButton()); - buttonData.setFacing(buttonFacing); - button.getBlock().setBlockData(buttonData); + if (!MaterialHelper.isButtonCompatible(button.getType())) { + @NotNull List possibleMaterials = MaterialHelper.specifiersToMaterials( + portal.getGate().getPortalButtonMaterials()).stream().toList(); + Material buttonType = ListHelper.getRandom(possibleMaterials); + + Directional buttonData = (Directional) Bukkit.createBlockData(buttonType); + buttonData.setFacing(buttonFacing); + button.getBlock().setBlockData(buttonData); + } portal.getStructure().setButton(button); }