diff --git a/Towny/src/main/java/com/palmergames/bukkit/config/ConfigNodes.java b/Towny/src/main/java/com/palmergames/bukkit/config/ConfigNodes.java index 2ce1702bc90..f7ebfb0c132 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/config/ConfigNodes.java +++ b/Towny/src/main/java/com/palmergames/bukkit/config/ConfigNodes.java @@ -1,5 +1,7 @@ package com.palmergames.bukkit.config; +import java.util.HashMap; + public enum ConfigNodes { VERSION_HEADER("version", "", ""), VERSION( @@ -3263,16 +3265,48 @@ public enum ConfigNodes { "27", "", "# The width of the map shown in /towny map and /res toggle map.", - "# Minimum 7, maximum 27, only odd numbers are accepted."); + "# Minimum 7, maximum 27, only odd numbers are accepted."), + CUSTOM_LISTS("custom_lists", "", "", + "# This section of the config allows you to specify custom lists of blocks or entities, that can then be used elsewhere in the config.", + "# A custom syntax is used to specify whether to include or exclude certain patterns from the resulting set.", + "#", + "# Format:", + "# *string: Includes all elements that end with 'string'", + "# string*: Includes all elements that start with 'string'", + "# !*string: Excludes all elements that don't end with 'string'", + "# !string*: Excludes all elements that don't start with 'string'", + "# #tag: Includes all elements that are contained in the given tag, valid tags can be found here: https://github.com/misode/mcmeta/tree/data/data/minecraft/tags in the blocks or entity_type folders.", + "# !#tag: Excludes all elements that are contained in the given tag", + "# +string: Includes the element with name 'string'", + "# -string: Excludes the element with name 'string'", + "# ~string: Includes all elements that contain 'string' in the name", + "# !~string: Excludes all elements that contain 'string' in the name", + "# c:class: Includes all elements that are instances of the given class"), + CUSTOM_LISTS_ITEM_LISTS("custom_lists.item_lists", + new HashMap<>(), + "", + "# Define your custom item lists here.", + "# Example:", + "#", + "# item_lists:", + "# chests: '~chest'"), + CUSTOM_LISTS_ENTITY_LISTS("custom_lists.entity_lists", + new HashMap<>(), + "", + "# Define your custom entity lists here.", + "# Example:", + "#", + "# entity_lists:", + "# animals: 'c:Animals'"); - private final String Root; - private final String Default; - private String[] comments; + private final String root; + private final Object defaultValue; + private final String[] comments; - ConfigNodes(String root, String def, String... comments) { + ConfigNodes(String root, Object def, String... comments) { - this.Root = root; - this.Default = def; + this.root = root; + this.defaultValue = def; this.comments = comments; } @@ -3283,7 +3317,7 @@ public enum ConfigNodes { */ public String getRoot() { - return Root; + return root; } /** @@ -3293,7 +3327,16 @@ public String getRoot() { */ public String getDefault() { - return Default; + return defaultValue.toString(); + } + + /** + * Retrieves the default value for a config path + * + * @return The default value for a config path + */ + public Object defaultValue() { + return this.defaultValue; } /** diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/TownySettings.java b/Towny/src/main/java/com/palmergames/bukkit/towny/TownySettings.java index 8e78817bee6..b45ba52e15d 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/TownySettings.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/TownySettings.java @@ -9,6 +9,7 @@ import com.palmergames.bukkit.towny.event.TownUpkeepPenalityCalculationEvent; import com.palmergames.bukkit.towny.exceptions.TownyException; import com.palmergames.bukkit.towny.exceptions.initialization.TownyInitException; +import com.palmergames.bukkit.towny.object.AbstractRegistryList; import com.palmergames.bukkit.towny.object.Nation; import com.palmergames.bukkit.towny.object.Resident; import com.palmergames.bukkit.towny.object.Town; @@ -24,16 +25,20 @@ import com.palmergames.bukkit.towny.utils.MapUtil; import com.palmergames.bukkit.util.BukkitTools; import com.palmergames.bukkit.util.Colors; +import com.palmergames.bukkit.util.EntityLists; import com.palmergames.bukkit.util.ItemLists; import com.palmergames.bukkit.util.Version; import com.palmergames.util.FileMgmt; import com.palmergames.util.StringMgmt; import com.palmergames.util.TimeTools; +import org.bukkit.Keyed; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.Registry; +import org.bukkit.Tag; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.EntityType; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -49,14 +54,17 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; public class TownySettings { @@ -101,8 +109,8 @@ public record NationLevel( private static final SortedMap configTownLevel = Collections.synchronizedSortedMap(new TreeMap<>(Collections.reverseOrder())); private static final SortedMap configNationLevel = Collections.synchronizedSortedMap(new TreeMap<>(Collections.reverseOrder())); - private static final Set itemUseMaterials = new HashSet<>(); - private static final Set switchUseMaterials = new HashSet<>(); + private static final Set itemUseMaterials = new LinkedHashSet<>(); + private static final Set switchUseMaterials = new LinkedHashSet<>(); private static final List> protectedMobs = new ArrayList<>(); private static final Map> CONFIG_RELOAD_LISTENERS = new HashMap<>(); @@ -453,6 +461,7 @@ public static void loadConfig(Path configPath, String version) { config.save(); + loadCustomRegistryLists(); loadSwitchAndItemUseMaterialsLists(); loadProtectedMobsList(); ChunkNotification.loadFormatStrings(); @@ -489,38 +498,30 @@ private static void loadSwitchAndItemUseMaterialsLists() { * Scan over them and replace any grouping with the contents of the group. * Add single item or grouping to SwitchUseMaterials. */ - List switches = getStrArr(ConfigNodes.PROT_SWITCH_MAT); - for (String matName : switches) { - if (ItemLists.GROUPS.contains(matName)) { - switchUseMaterials.addAll(ItemLists.getGrouping(matName)); - } else { - Material material = BukkitTools.matchRegistry(Registry.MATERIAL, matName); - if (material != null) - switchUseMaterials.add(material); - } - } + switchUseMaterials.addAll(toMaterialSet(getStrArr(ConfigNodes.PROT_SWITCH_MAT))); /* * Load items from config value. * Scan over them and replace any grouping with the contents of the group. * Add single item or grouping to ItemUseMaterials. */ - List items = getStrArr(ConfigNodes.PROT_ITEM_USE_MAT); - for (String matName : items) { - if (ItemLists.GROUPS.contains(matName)) { - itemUseMaterials.addAll(ItemLists.getGrouping(matName)); - } else { - Material material = BukkitTools.matchRegistry(Registry.MATERIAL, matName); - if (material != null) - itemUseMaterials.add(material); - } - } + itemUseMaterials.addAll(toMaterialSet(getStrArr(ConfigNodes.PROT_ITEM_USE_MAT))); + } + + @Deprecated + private static Set toEntityTypeSet$$bridge$$public(final List entityList) { + return toEntityTypeSet(entityList); } - public static Set toEntityTypeSet(final List entityList) { - final Set entities = new HashSet<>(); + public static Set toEntityTypeSet(final Collection entityList) { + final Set entities = new LinkedHashSet<>(); for (final String entityName : entityList) { + if (EntityLists.hasGroup(entityName)) { + entities.addAll(EntityLists.getGrouping(entityName)); + continue; + } + final EntityType type = BukkitTools.matchRegistry(Registry.ENTITY_TYPE, switch (entityName.toLowerCase(Locale.ROOT)) { // This is needed because some of the entity type fields don't/didn't match the actual key. // @@ -553,15 +554,20 @@ public static Set toEntityTypeSet(final List entityList) { return entities; } - public static Collection toMaterialSet(List materialList) { - Set materials = new HashSet<>(); + @Deprecated + private static Collection toMaterialSet$$bridge$$public(List materialList) { + return toMaterialSet(materialList); + } + + public static Collection toMaterialSet(Collection materialList) { + Set materials = new LinkedHashSet<>(); for (String materialName : materialList) { if (materialName.isEmpty()) continue; - if (ItemLists.GROUPS.contains(materialName.toUpperCase(Locale.ROOT))) { - materials.addAll(ItemLists.getGrouping(materialName.toUpperCase(Locale.ROOT))); + if (ItemLists.hasGroup(materialName)) { + materials.addAll(ItemLists.getGrouping(materialName)); } else { Material material = BukkitTools.matchRegistry(Registry.MATERIAL, materialName); if (material != null) @@ -674,6 +680,19 @@ public static long getMillis(ConfigNodes node) { return 1; } } + + public static Map getMap(ConfigNodes node) { + final Map map = new HashMap<>(); + + final ConfigurationSection section = config.getConfigurationSection(node.getRoot()); + if (section == null) + return map; + + for (String key : section.getKeys(false)) + map.put(key, section.getString(key)); + + return map; + } public static void addComment(String root, String... comments) { @@ -707,10 +726,10 @@ private static void setDefaults(String version, Path configPath) { } else if (root.getRoot().equals(ConfigNodes.LAST_RUN_VERSION.getRoot())) { setNewProperty(root.getRoot(), getLastRunVersion(version)); } else if (root.getRoot().equals(ConfigNodes.TOWNBLOCKTYPES_TYPES.getRoot())) { - setNewProperty(root.getRoot(), root.getDefault()); + setNewProperty(root.getRoot(), root.defaultValue()); setTownBlockTypes(); } else - setNewProperty(root.getRoot(), (config.get(root.getRoot().toLowerCase(Locale.ROOT)) != null) ? config.get(root.getRoot().toLowerCase(Locale.ROOT)) : root.getDefault()); + setNewProperty(root.getRoot(), (config.get(root.getRoot().toLowerCase(Locale.ROOT)) != null) ? config.get(root.getRoot().toLowerCase(Locale.ROOT)) : root.defaultValue()); } @@ -1089,6 +1108,98 @@ private static void setTownBlockTypes() { newConfig.set(ConfigNodes.TOWNBLOCKTYPES_TYPES.getRoot(), config.get(ConfigNodes.TOWNBLOCKTYPES_TYPES.getRoot())); } + private static void loadCustomRegistryLists() { + ItemLists.clearCustomGroups(); + for (Map.Entry entry : getMap(ConfigNodes.CUSTOM_LISTS_ITEM_LISTS).entrySet()) + ItemLists.addGroup(entry.getKey(), constructRegistryList(ItemLists.newBuilder(), Tag.REGISTRY_BLOCKS, Arrays.asList(entry.getValue().split(",")), mat -> mat.data)); + + EntityLists.clearCustomGroups(); + for (Map.Entry entry : getMap(ConfigNodes.CUSTOM_LISTS_ENTITY_LISTS).entrySet()) + EntityLists.addGroup(entry.getKey(), constructRegistryList(EntityLists.newBuilder(), Tag.REGISTRY_ENTITY_TYPES, Arrays.asList(entry.getValue().split(",")), EntityType::getEntityClass)); + } + + @VisibleForTesting + public static > F constructRegistryList(final AbstractRegistryList.Builder builder, final String registryName, final Iterable elements, final Function> classExtractor) throws TownyInitException { + for (final String e : elements) { + final String element = e.trim(); + + if (element.startsWith("*")) + builder.endsWith(element.substring(1)); + else if (element.startsWith("!*")) + builder.notEndsWith(element.substring(2)); + else if (element.endsWith("*")) + builder.startsWith(element.substring(0, element.length() - 1)); + else if (element.startsWith("!") && element.endsWith("*")) + builder.notStartsWith(element.substring(1, element.length() - 1)); + else if (element.startsWith("#")) + builder.withTag(registryName, Optional.ofNullable(NamespacedKey.fromString(element.substring(1))).orElseThrow(() -> new TownyInitException(element.substring(1) + " is not a valid key", TownyInitException.TownyError.MAIN_CONFIG))); + else if (element.startsWith("!#")) + builder.excludeTag(registryName, Optional.ofNullable(NamespacedKey.fromString(element.substring(2))).orElseThrow(() -> new TownyInitException(element.substring(2) + " is not a valid key", TownyInitException.TownyError.MAIN_CONFIG))); + else if (element.startsWith("+")) + builder.add(element.substring(1)); + else if (element.startsWith("-")) + builder.not(element.substring(1)); + else if (element.startsWith("~")) + builder.contains(element.substring(1)); + else if (element.startsWith("!~")) + builder.notContains(element.substring(2)); + else if (element.startsWith("c:") || element.startsWith("!c:")) { + int substr = 2; + boolean add = true; + + if (element.startsWith("!")) { + substr++; + add = false; + } + + final String className = element.substring(substr); + boolean classFound = false; + + // Check certain packages so that fully qualified names aren't required, i.e. 'Animals' will work. + for (String packageName : REGISTRY_LIST_PACKAGES) + classFound |= checkClass(builder, classExtractor, packageName + "." + className, add); + + // Check exact class, since this could be a fully qualified name already. + classFound |= checkClass(builder, classExtractor, className, add); + + // The user's class name might be wrong/wrongly cased, let them know about it. + if (!classFound) + throw new TownyInitException("Could not find class named " + className, TownyInitException.TownyError.MAIN_CONFIG); + } else + TownyMessaging.sendErrorMsg("Invalid format for element " + element); + } + + return builder.build(); + } + + private static > boolean checkClass(AbstractRegistryList.Builder builder, Function> classExtractor, String className, boolean add) { + final Class desired; + try { + desired = Class.forName(className); + } catch (ClassNotFoundException e) { + return false; + } + + Predicate predicate = t -> { + final Class clazz = classExtractor.apply(t); + return clazz != null && desired.isAssignableFrom(clazz); + }; + + if (add) + builder.addIf(predicate); + else + builder.removeIf(predicate); + + return true; + } + + private static final List REGISTRY_LIST_PACKAGES = Arrays.asList( + "org.bukkit.entity", + "org.bukkit.entity.minecart", + "org.bukkit.block.data", + "org.bukkit.block.data.type" + ); + public static String getDefaultFarmblocks() { Set farmMaterials = new HashSet<>(); farmMaterials.addAll(ItemLists.SAPLINGS.getMaterialNameCollection()); @@ -1827,9 +1938,9 @@ public static boolean isFireSpreadBypassMaterial(String mat) { return getFireSpreadBypassMaterials().contains(mat); } - public static Collection getUnclaimedZoneIgnoreMaterials() { + public static Collection getUnclaimedZoneIgnoreMaterials() { - return toMaterialSet(getStrArr(ConfigNodes.UNCLAIMED_ZONE_IGNORE)); + return getStrArr(ConfigNodes.UNCLAIMED_ZONE_IGNORE); } public static List> getProtectedEntityTypes() { @@ -1852,7 +1963,7 @@ private static void setNewProperty(String root, Object value) { TownyMessaging.sendDebugMsg("value is null for " + root.toLowerCase(Locale.ROOT)); value = ""; } - newConfig.set(root.toLowerCase(Locale.ROOT), value.toString()); + newConfig.set(root.toLowerCase(Locale.ROOT), value); } public static void setLanguage(String lang) { diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyFlatFileSource.java b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyFlatFileSource.java index 4518e7b6108..cf66c2ffc3c 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyFlatFileSource.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyFlatFileSource.java @@ -1505,7 +1505,7 @@ public boolean loadWorld(TownyWorld world) { if (line != null) try { List mats = new ArrayList<>(); - for (String s : line.split("#")) + for (String s : line.split("#,")) if (!s.isEmpty()) mats.add(s); @@ -2359,8 +2359,8 @@ public boolean saveWorld(TownyWorld world) { list.add("# The following are blocks that will bypass the above build, destroy, switch and itemuse settings."); // Unclaimed Zone Ignore Ids - if (world.getUnclaimedZoneIgnoreMaterials() != null) - list.add("unclaimedZoneIgnoreIds=" + StringMgmt.join(world.getUnclaimedZoneIgnoreMaterials(), ",")); + if (world.getUnclaimedZoneIgnoreMaterialNames() != null) + list.add("unclaimedZoneIgnoreIds=" + StringMgmt.join(world.getUnclaimedZoneIgnoreMaterialNames(), ",")); // PlotManagement Delete list.add(""); @@ -2368,16 +2368,16 @@ public boolean saveWorld(TownyWorld world) { // Using PlotManagement Delete list.add("usingPlotManagementDelete=" + world.isUsingPlotManagementDelete()); // Plot Management Delete Ids - if (world.getPlotManagementDeleteIds() != null) - list.add("plotManagementDeleteIds=" + StringMgmt.join(world.getPlotManagementDeleteIds(), ",")); + if (world.getPlotManagementDeleteNames() != null) + list.add("plotManagementDeleteIds=" + StringMgmt.join(world.getPlotManagementDeleteNames(), ",")); // EntityType removal on unclaim. list.add(""); list.add("# The following settings control what EntityTypes are deleted upon a townblock being unclaimed"); list.add("# Valid EntityTypes are listed here: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/EntityType.html"); list.add("isDeletingEntitiesOnUnclaim=" + world.isDeletingEntitiesOnUnclaim()); - if (world.getUnclaimDeleteEntityTypes() != null) - list.add("unclaimDeleteEntityTypes=" + StringMgmt.join(BukkitTools.convertKeyedToString(world.getUnclaimDeleteEntityTypes()), ",")); + if (world.getUnclaimDeleteEntityTypeNames() != null) + list.add("unclaimDeleteEntityTypes=" + StringMgmt.join(world.getUnclaimDeleteEntityTypeNames(), ",")); // PlotManagement list.add(""); @@ -2385,8 +2385,8 @@ public boolean saveWorld(TownyWorld world) { // Using PlotManagement Mayor Delete list.add("usingPlotManagementMayorDelete=" + world.isUsingPlotManagementMayorDelete()); // Plot Management Mayor Delete - if (world.getPlotManagementMayorDelete() != null) - list.add("plotManagementMayorDelete=" + StringMgmt.join(world.getPlotManagementMayorDelete(), ",")); + if (world.getPlotManagementMayorDeleteNames() != null) + list.add("plotManagementMayorDelete=" + StringMgmt.join(world.getPlotManagementMayorDeleteNames(), ",")); // PlotManagement Revert list.add(""); @@ -2397,11 +2397,11 @@ public boolean saveWorld(TownyWorld world) { list.add("# Any block Id's listed here will not be respawned. Instead it will revert to air. This list also world on the WildRegen settings below."); // Plot Management Ignore Ids - if (world.getPlotManagementIgnoreIds() != null) - list.add("plotManagementIgnoreIds=" + StringMgmt.join(world.getPlotManagementIgnoreIds(), ",")); + if (world.getPlotManagementIgnoreNames() != null) + list.add("plotManagementIgnoreIds=" + StringMgmt.join(world.getPlotManagementIgnoreNames(), ",")); // Revert on Unclaim whitelisted Materials. - if (world.getRevertOnUnclaimWhitelistMaterials() != null) - list.add("revertOnUnclaimWhitelistMaterials=" + StringMgmt.join(world.getRevertOnUnclaimWhitelistMaterials(), "#")); + if (world.getRevertOnUnclaimWhitelistMaterialNames() != null) + list.add("revertOnUnclaimWhitelistMaterials=" + StringMgmt.join(world.getRevertOnUnclaimWhitelistMaterialNames(), ",")); // PlotManagement Wild Regen list.add(""); @@ -2412,8 +2412,8 @@ public boolean saveWorld(TownyWorld world) { list.add("# The list of entities whose explosions would be reverted."); // Wilderness Explosion Protection entities - if (world.getPlotManagementWildRevertEntities() != null) - list.add("PlotManagementWildRegenEntities=" + StringMgmt.join(BukkitTools.convertKeyedToString(world.getPlotManagementWildRevertEntities()), ",")); + if (world.getPlotManagementWildRevertEntityNames() != null) + list.add("PlotManagementWildRegenEntities=" + StringMgmt.join(world.getPlotManagementWildRevertEntityNames(), ",")); list.add("# If enabled any damage caused by block explosions will repair itself."); // Using PlotManagement Wild Block Regen @@ -2421,18 +2421,18 @@ public boolean saveWorld(TownyWorld world) { list.add("# The list of blocks whose explosions would be reverted."); // Wilderness Explosion Protection blocks - if (world.getPlotManagementWildRevertBlocks() != null) - list.add("PlotManagementWildRegenBlocks=" + StringMgmt.join(world.getPlotManagementWildRevertBlocks(), ",")); + if (world.getPlotManagementWildRevertBlockNames() != null) + list.add("PlotManagementWildRegenBlocks=" + StringMgmt.join(world.getPlotManagementWildRevertBlockNames(), ",")); list.add("# The list of blocks to regenerate. (if empty all blocks will regenerate)"); // Wilderness Explosion Protection entities - if (world.getPlotManagementWildRevertBlockWhitelist() != null) - list.add("PlotManagementWildRegenBlockWhitelist=" + StringMgmt.join(world.getPlotManagementWildRevertBlockWhitelist(), ",")); + if (world.getPlotManagementWildRevertBlockWhitelistNames() != null) + list.add("PlotManagementWildRegenBlockWhitelist=" + StringMgmt.join(world.getPlotManagementWildRevertBlockWhitelistNames(), ",")); list.add("# The list of blocks to that should not get replaced when an explosion is reverted in the wilderness, ie: a chest placed in a creeper hole that is reverting."); // Wilderness Explosion materials to not overwrite. - if (world.getWildRevertMaterialsToNotOverwrite() != null) - list.add("wildRegenBlocksToNotOverwrite=" + StringMgmt.join(world.getWildRevertMaterialsToNotOverwrite(), ",")); + if (world.getWildRevertMaterialsToNotOverwriteNames() != null) + list.add("wildRegenBlocksToNotOverwrite=" + StringMgmt.join(world.getWildRevertMaterialsToNotOverwriteNames(), ",")); list.add("# The delay after which the explosion reverts will begin."); // Using PlotManagement Wild Regen Delay diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownySQLSource.java b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownySQLSource.java index c93a972d3ac..1a77282645f 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownySQLSource.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownySQLSource.java @@ -2581,53 +2581,51 @@ public synchronized boolean saveWorld(TownyWorld world) { nat_hm.put("unclaimedZoneName", world.getUnclaimedZoneName()); // Unclaimed Zone Ignore Ids - if (world.getUnclaimedZoneIgnoreMaterials() != null) - nat_hm.put("unclaimedZoneIgnoreIds", StringMgmt.join(world.getUnclaimedZoneIgnoreMaterials(), "#")); + if (world.getUnclaimedZoneIgnoreMaterialNames() != null) + nat_hm.put("unclaimedZoneIgnoreIds", StringMgmt.join(world.getUnclaimedZoneIgnoreMaterialNames(), "#")); // Deleting EntityTypes from Townblocks on Unclaim. nat_hm.put("isDeletingEntitiesOnUnclaim", world.isDeletingEntitiesOnUnclaim()); - if (world.getUnclaimDeleteEntityTypes() != null) - nat_hm.put("unclaimDeleteEntityTypes", StringMgmt.join(BukkitTools.convertKeyedToString(world.getUnclaimDeleteEntityTypes()), "#")); + if (world.getUnclaimDeleteEntityTypeNames() != null) + nat_hm.put("unclaimDeleteEntityTypes", StringMgmt.join(world.getUnclaimDeleteEntityTypeNames(), "#")); // Using PlotManagement Delete nat_hm.put("usingPlotManagementDelete", world.isUsingPlotManagementDelete()); // Plot Management Delete Ids - if (world.getPlotManagementDeleteIds() != null) - nat_hm.put("plotManagementDeleteIds", StringMgmt.join(world.getPlotManagementDeleteIds(), "#")); + if (world.getPlotManagementDeleteNames() != null) + nat_hm.put("plotManagementDeleteIds", StringMgmt.join(world.getPlotManagementDeleteNames(), "#")); // Using PlotManagement Mayor Delete nat_hm.put("usingPlotManagementMayorDelete", world.isUsingPlotManagementMayorDelete()); // Plot Management Mayor Delete - if (world.getPlotManagementMayorDelete() != null) - nat_hm.put("plotManagementMayorDelete", StringMgmt.join(world.getPlotManagementMayorDelete(), "#")); + if (world.getPlotManagementMayorDeleteNames() != null) + nat_hm.put("plotManagementMayorDelete", StringMgmt.join(world.getPlotManagementMayorDeleteNames(), "#")); // Using PlotManagement Revert nat_hm.put("usingPlotManagementRevert", world.isUsingPlotManagementRevert()); // Plot Management Ignore Ids - if (world.getPlotManagementIgnoreIds() != null) - nat_hm.put("plotManagementIgnoreIds", StringMgmt.join(world.getPlotManagementIgnoreIds(), "#")); + if (world.getPlotManagementIgnoreNames() != null) + nat_hm.put("plotManagementIgnoreIds", StringMgmt.join(world.getPlotManagementIgnoreNames(), "#")); // Revert on Unclaim whitelisted materials - if (world.getRevertOnUnclaimWhitelistMaterials() != null) - nat_hm.put("revertOnUnclaimWhitelistMaterials", StringMgmt.join(world.getRevertOnUnclaimWhitelistMaterials(), "#")); + if (world.getRevertOnUnclaimWhitelistMaterialNames() != null) + nat_hm.put("revertOnUnclaimWhitelistMaterials", StringMgmt.join(world.getRevertOnUnclaimWhitelistMaterialNames(), "#")); // Using PlotManagement Wild Regen nat_hm.put("usingPlotManagementWildRegen", world.isUsingPlotManagementWildEntityRevert()); // Wilderness Explosion Protection entities - if (world.getPlotManagementWildRevertEntities() != null) - nat_hm.put("PlotManagementWildRegenEntities", StringMgmt.join(BukkitTools.convertKeyedToString(world.getPlotManagementWildRevertEntities()), "#")); + if (world.getPlotManagementWildRevertEntityNames() != null) + nat_hm.put("PlotManagementWildRegenEntities", StringMgmt.join(world.getPlotManagementWildRevertEntityNames(), "#")); // Wilderness Explosion Protection Block Whitelist - if (world.getPlotManagementWildRevertBlockWhitelist() != null) - nat_hm.put("PlotManagementWildRegenBlockWhitelist", - StringMgmt.join(world.getPlotManagementWildRevertBlockWhitelist(), "#")); + if (world.getPlotManagementWildRevertBlockWhitelistNames() != null) + nat_hm.put("PlotManagementWildRegenBlockWhitelist", StringMgmt.join(world.getPlotManagementWildRevertBlockWhitelistNames(), "#")); // Wilderness Explosion Protection Materials to not overwrite. - if (world.getWildRevertMaterialsToNotOverwrite() != null) - nat_hm.put("wildRegenBlocksToNotOverwrite", - StringMgmt.join(world.getWildRevertMaterialsToNotOverwrite(), "#")); + if (world.getWildRevertMaterialsToNotOverwriteNames() != null) + nat_hm.put("wildRegenBlocksToNotOverwrite", StringMgmt.join(world.getWildRevertMaterialsToNotOverwriteNames(), "#")); // Using PlotManagement Wild Regen Delay nat_hm.put("plotManagementWildRegenSpeed", world.getPlotManagementWildRevertDelay()); @@ -2636,9 +2634,8 @@ public synchronized boolean saveWorld(TownyWorld world) { nat_hm.put("usingPlotManagementWildRegenBlocks", world.isUsingPlotManagementWildBlockRevert()); // Wilderness Explosion Protection blocks - if (world.getPlotManagementWildRevertBlocks() != null) - nat_hm.put("PlotManagementWildRegenBlocks", - StringMgmt.join(world.getPlotManagementWildRevertBlocks(), "#")); + if (world.getPlotManagementWildRevertBlockNames() != null) + nat_hm.put("PlotManagementWildRegenBlocks", StringMgmt.join(world.getPlotManagementWildRevertBlockNames(), "#")); // Using Towny nat_hm.put("usingTowny", world.isUsingTowny()); diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/object/AbstractRegistryList.java b/Towny/src/main/java/com/palmergames/bukkit/towny/object/AbstractRegistryList.java index b16750898e7..777766e370f 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/object/AbstractRegistryList.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/object/AbstractRegistryList.java @@ -1,18 +1,36 @@ package com.palmergames.bukkit.towny.object; import com.palmergames.bukkit.towny.TownyMessaging; +import com.palmergames.bukkit.towny.TownySettings; import com.palmergames.bukkit.util.BukkitTools; +import com.palmergames.bukkit.util.EntityLists; +import com.palmergames.bukkit.util.ItemLists; +import com.palmergames.util.JavaUtil; import org.bukkit.Bukkit; import org.bukkit.Keyed; +import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.Registry; import org.bukkit.Tag; +import org.bukkit.entity.EntityType; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.AbstractCollection; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; @@ -58,10 +76,7 @@ public static class Builder> private final Class clazz; private final Function, F> convertFunction; - // Predicates where all should match, this is used for functions that exclude certain elements. (notStartsWith, contains, etc.) - private final Set> allMatchPredicates = new HashSet<>(); - // Predicates where only 1 has to match, this is used for functions that include new elements. (endsWith, startsWith) - private final Set> anyMatchPredicates = new HashSet<>(); + private final List> layers = new ArrayList<>(); /** * @param registry The bukkit registry, used for matching strings into {@link T}. @@ -75,53 +90,80 @@ public Builder(Registry registry, Class clazz, Function, F> } public F build() { + Set allValues = new HashSet<>(); + + for (final T value : registry) { + allValues.add(value); + } + + allValues = Collections.unmodifiableSet(allValues); + final Set matches = new HashSet<>(); - if (!allMatchPredicates.isEmpty() || !anyMatchPredicates.isEmpty()) { - for (final T element : registry) - if (allMatchPredicates.stream().allMatch(predicate -> predicate.test(element)) && (anyMatchPredicates.isEmpty() || anyMatchPredicates.stream().anyMatch(predicate -> predicate.test(element)))) - matches.add(element); + for (final LayerConsumer layer : layers) { + layer.accept(matches, allValues); } return convertFunction.apply(matches); } public Builder startsWith(String startingWith) { - anyMatchPredicates.add((s) -> s.getKey().getKey().regionMatches(true, 0, startingWith, 0, startingWith.length())); + layers.add((currentSet, allPossible) -> { + for (final T value : allPossible) + if (value.getKey().getKey().regionMatches(true, 0, startingWith, 0, startingWith.length())) + currentSet.add(value); + }); return this; } public Builder endsWith(@NotNull String endingWith) { final String endingWithLower = endingWith.toLowerCase(Locale.ROOT); - anyMatchPredicates.add((s) -> s.getKey().getKey().endsWith(endingWithLower)); + layers.add((currentSet, allPossible) -> { + for (final T value : allPossible) + if (value.getKey().getKey().endsWith(endingWithLower)) + currentSet.add(value); + }); return this; } public Builder not(@NotNull String name) { - allMatchPredicates.add((s) -> !s.getKey().getKey().equalsIgnoreCase(name)); + final T value = get(name); + if (value != null) + layers.add((currentSet, allPossible) -> currentSet.remove(value)); + return this; } public Builder notStartsWith(@NotNull String notStartingWith) { - allMatchPredicates.add((s) -> !s.getKey().getKey().regionMatches(true, 0, notStartingWith, 0, notStartingWith.length())); + layers.add((currentSet, allPossible) -> { + currentSet.removeIf(value -> value.getKey().getKey().regionMatches(true, 0, notStartingWith, 0, notStartingWith.length())); + }); return this; } public Builder notEndsWith(@NotNull String notEndingWith) { final String notEndingLower = notEndingWith.toLowerCase(Locale.ROOT); - allMatchPredicates.add((s) -> !s.getKey().getKey().endsWith(notEndingLower)); + layers.add((currentSet, allPossible) -> { + currentSet.removeIf(value -> value.getKey().getKey().endsWith(notEndingLower)); + }); return this; } public Builder contains(@NotNull String containing) { final String containingLower = containing.toLowerCase(Locale.ROOT); - allMatchPredicates.add((s) -> s.getKey().getKey().contains(containingLower)); + layers.add((currentSet, allPossible) -> { + for (final T value : allPossible) + if (value.getKey().getKey().contains(containingLower)) + currentSet.add(value); + }); return this; } public Builder notContains(@NotNull String notContaining) { final String notContainingLower = notContaining.toLowerCase(Locale.ROOT); - allMatchPredicates.add((s) -> !s.getKey().getKey().contains(notContainingLower)); + layers.add((currentSet, allPossible) -> { + currentSet.removeIf(value -> value.getKey().getKey().contains(notContainingLower)); + }); return this; } @@ -135,7 +177,7 @@ public Builder withTag(@NotNull String registry, @NotNull NamespacedKey ke final Tag tag = Bukkit.getServer().getTag(registry, key, this.clazz); if (tag != null) - anyMatchPredicates.add(tag::isTagged); + layers.add((currentSet, allPossible) -> currentSet.addAll(tag.getValues())); return this; } @@ -149,7 +191,7 @@ public Builder excludeTag(@NotNull String registry, @NotNull NamespacedKey final Tag tag = Bukkit.getServer().getTag(registry, key, this.clazz); if (tag != null) - allMatchPredicates.add(s -> !tag.isTagged(s)); + layers.add((currentSet, allPossible) -> currentSet.removeAll(tag.getValues())); return this; } @@ -160,15 +202,13 @@ public Builder excludeTag(@NotNull String registry, @NotNull NamespacedKey */ public Builder add(@NotNull String... names) { for (String name : names) { - final T match = BukkitTools.matchRegistry(this.registry, name); + final T match = get(name); if (match != null) - anyMatchPredicates.add(t -> t.equals(match)); + layers.add((currentSet, allPossible) -> currentSet.add(match)); else { try { TownyMessaging.sendDebugMsg("Expected element with name '" + name + "' was not found in the " + this.clazz.getSimpleName() + " registry."); } catch (final Exception ignored) {} - - anyMatchPredicates.add(t -> false); } } @@ -180,15 +220,33 @@ public Builder add(@NotNull String... names) { * @param list list to add. */ public Builder includeList(@NotNull AbstractRegistryList list) { - for (final T element : list.tagged) { - anyMatchPredicates.add(t -> t.equals(element)); - } + layers.add((currentSet, allPossible) -> { + currentSet.addAll(list.tagged()); + }); return this; } + + public Builder retainList(@NotNull AbstractRegistryList list) { + layers.add(((currentSet, allPossible) -> { + currentSet.retainAll(list.tagged()); + })); + return this; + } - public Builder filter(@NotNull Predicate predicate) { - allMatchPredicates.add(predicate); + public Builder removeIf(@NotNull Predicate predicate) { + layers.add((currentSet, allPossible) -> { + currentSet.removeIf(predicate); + }); + return this; + } + + public Builder addIf(@NotNull Predicate predicate) { + layers.add((currentSet, allPossible) -> { + for (final T value : allPossible) + if (predicate.test(value)) + currentSet.add(value); + }); return this; } @@ -198,5 +256,170 @@ public Builder conditionally(@NotNull BooleanSupplier supplier, @NotNull C return this; } + + @Nullable + private T get(final String name) { + return BukkitTools.matchRegistry(this.registry, name); + } + + private interface LayerConsumer extends BiConsumer, Collection> { + @Override + void accept(final @NotNull Collection currentSet, final @NotNull @Unmodifiable Collection allPossible); + } + } + + /** + * A class to allow round tripping with string names + */ + @SuppressWarnings("unchecked") + public static class CompactableCollection extends AbstractCollection { + private final List names = new ArrayList<>(); + private final Class clazz; + + private final Set cachedValues = new HashSet<>(); + + public CompactableCollection(Class clazz) { + this.clazz = clazz; + } + + @ApiStatus.Internal + protected static CompactableCollection materials() { + return new CompactableCollection<>(Material.class); + } + + protected static CompactableCollection materials(final @NotNull Collection materials) { + return JavaUtil.make(materials(), m -> m.setNames(materials)); + } + + @ApiStatus.Internal + protected static CompactableCollection entityTypes() { + return new CompactableCollection<>(EntityType.class); + } + + @ApiStatus.Internal + protected static CompactableCollection entityTypes(final @NotNull Collection entityTypes) { + return JavaUtil.make(entityTypes(), m -> m.setNames(entityTypes)); + } + + public void setNames(final Collection names) { + synchronized (this.names) { + this.names.clear(); + this.names.addAll(names); + } + + updateCache(); + } + + public Collection getNames() { + return this.names; + } + + @Override + public @NotNull Iterator iterator() { + synchronized (cachedValues) { + return cachedValues.iterator(); + } + } + + @Override + public int size() { + synchronized (cachedValues) { + return cachedValues.size(); + } + } + + @Override + public boolean add(final T value) { + Objects.requireNonNull(value, "value"); + final String asString = BukkitTools.keyAsString(value.getKey()); + + //noinspection ConstantValue + final boolean result = !this.names.contains(asString) && this.names.add(asString); + if (result) updateCache(); + + return result; + } + + @Override + public boolean remove(final Object object) { + Objects.requireNonNull(object, "value"); + + if (!object.getClass().equals(this.clazz) || !(object instanceof Keyed key)) + throw new ClassCastException(object.getClass() + " is not a " + this.clazz); + + final T value = (T) object; + final String asString = BukkitTools.keyAsString(value.getKey()); + + final boolean result = this.names.remove(BukkitTools.keyAsString(key.getKey())); + if (result) updateCache(); + + // Value is still part of the cached values despite being removed from names, which must mean they're part of a group + if (this.cachedValues.contains(value) && !this.names.contains(asString)) { + // Find the group + for (final Map.Entry> group : getAllGroups().entrySet()) { + int index = this.names.indexOf(group.getKey()); + + if (index != -1 && group.getValue().contains(value)) { + this.names.remove(index); + + // Create a copy of the group values without the element that's being removed + Set replacingValues = new HashSet<>(group.getValue()); + replacingValues.remove(value); + + this.names.addAll(index, BukkitTools.convertKeyedToString(replacingValues)); + updateCache(); + + return true; + } + } + + } + + return result; + } + + /** + * @return {@code true} if names were able to be compacted into a group + */ + public boolean compact() { + final int oldSize = this.names.size(); + + Collection newNames = BukkitTools.convertKeyedToString(this.cachedValues); + + for (Map.Entry> group : getAllGroups().entrySet()) { + Collection asString = BukkitTools.convertKeyedToString(group.getValue()); + + if (newNames.containsAll(asString)) { + newNames.removeAll(asString); + newNames.add(group.getKey()); + } + } + + this.setNames(newNames); + + return this.names.size() != oldSize; + } + + private void updateCache() { + synchronized (cachedValues) { + cachedValues.clear(); + + if (this.clazz == Material.class) + cachedValues.addAll((Collection) TownySettings.toMaterialSet(this.names)); + else if (this.clazz == EntityType.class) + cachedValues.addAll((Collection) TownySettings.toEntityTypeSet(this.names)); + else + throw new UnsupportedOperationException("Unsupported class " + this.clazz); + } + } + + private Map> getAllGroups() { + if (this.clazz == Material.class) + return ItemLists.allGroups(); + else if (this.clazz == EntityType.class) { + return EntityLists.allGroups(); + } else + throw new UnsupportedOperationException("Unsupported class " + this.clazz); + } } } diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlockData.java b/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlockData.java index ebc2bf480fb..4bd16c0d9c5 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlockData.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlockData.java @@ -10,7 +10,7 @@ import java.util.Collection; import java.util.EnumSet; - import java.util.HashSet; + import java.util.LinkedHashSet; import java.util.Set; public class TownBlockData { @@ -18,9 +18,9 @@ public class TownBlockData { private NamedTextColor colour = null; private double cost = 0.0; private double tax = 0.0; - private final Set itemUseIds = new HashSet<>(); // List of item names that will trigger an item use test. - private final Set switchIds = new HashSet<>(); // List of item names that will trigger a switch test. - private final Set allowedBlocks = new HashSet<>(); // List of item names that will always be allowed. + private final Set itemUseIds = new LinkedHashSet<>(); // List of item names that will trigger an item use test. + private final Set switchIds = new LinkedHashSet<>(); // List of item names that will trigger a switch test. + private final Set allowedBlocks = new LinkedHashSet<>(); // List of item names that will always be allowed. public String getMapKey() { return mapKey; diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlockTypeHandler.java b/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlockTypeHandler.java index 346fdaecd01..2c9d20d5134 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlockTypeHandler.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlockTypeHandler.java @@ -156,7 +156,7 @@ private static Set loadMaterialList(String listName, String materialLi if (!materialList.isEmpty()) { Set set = new HashSet<>(); for (String materialName : materialList.split(",")) { - if (ItemLists.GROUPS.contains(materialName)) { + if (ItemLists.hasGroup(materialName)) { set.addAll(ItemLists.getGrouping(materialName)); continue; } diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownyWorld.java b/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownyWorld.java index d39827c98cf..aaf00e10d6f 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownyWorld.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownyWorld.java @@ -4,6 +4,7 @@ import com.palmergames.bukkit.towny.TownyUniverse; import com.palmergames.bukkit.towny.exceptions.NotRegisteredException; import com.palmergames.bukkit.towny.exceptions.TownyException; +import com.palmergames.bukkit.towny.object.AbstractRegistryList.CompactableCollection; import com.palmergames.bukkit.towny.object.TownyPermission.ActionType; import com.palmergames.bukkit.towny.object.metadata.CustomDataField; import com.palmergames.util.MathUtil; @@ -16,6 +17,7 @@ import org.bukkit.World; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; +import org.checkerframework.checker.units.qual.A; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -23,11 +25,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.UUID; public class TownyWorld extends TownyObject { @@ -36,28 +36,28 @@ public class TownyWorld extends TownyObject { private final HashMap towns = new HashMap<>(); private boolean isDeletingEntitiesOnUnclaim = TownySettings.isDeletingEntitiesOnUnclaim(); - private Set unclaimDeleteEntityTypes = null; + private CompactableCollection unclaimDeleteEntityTypes = null; private boolean isUsingPlotManagementDelete = TownySettings.isUsingPlotManagementDelete(); - private Set plotManagementDeleteIds = null; + private CompactableCollection plotManagementDeleteIds = null; private boolean isUsingPlotManagementMayorDelete = TownySettings.isUsingPlotManagementMayorDelete(); - private Set plotManagementMayorDelete = null; + private CompactableCollection plotManagementMayorDelete = null; private boolean isUsingPlotManagementRevert = TownySettings.isUsingPlotManagementRevert(); - private Set plotManagementIgnoreIds = null; - private Set revertOnUnclaimWhitelistMaterials = null; + private CompactableCollection plotManagementIgnoreIds = null; + private CompactableCollection revertOnUnclaimWhitelistMaterials = null; private boolean isUsingPlotManagementWildEntityRevert = TownySettings.isUsingPlotManagementWildEntityRegen(); private long plotManagementWildRevertDelay = TownySettings.getPlotManagementWildRegenDelay(); - private Set entityExplosionProtection = null; - private Set plotManagementWildRevertBlockWhitelist = null; - private Set wildRevertMaterialsToNotOverwrite = null; + private CompactableCollection entityExplosionProtection = null; + private CompactableCollection plotManagementWildRevertBlockWhitelist = null; + private CompactableCollection wildRevertMaterialsToNotOverwrite = null; private boolean isUsingPlotManagementWildBlockRevert = TownySettings.isUsingPlotManagementWildBlockRegen(); - private Set blockExplosionProtection = null; + private CompactableCollection blockExplosionProtection = null; - private Set unclaimedZoneIgnoreBlockMaterials = null; + private CompactableCollection unclaimedZoneIgnoreBlockMaterials = null; private Boolean unclaimedZoneBuild = null, unclaimedZoneDestroy = null, unclaimedZoneSwitch = null, unclaimedZoneItemUse = null; @@ -417,15 +417,21 @@ public boolean isDeletingEntitiesOnUnclaim() { return isDeletingEntitiesOnUnclaim; } + @NotNull public Collection getUnclaimDeleteEntityTypes() { if (unclaimDeleteEntityTypes == null) setUnclaimDeleteEntityTypes(TownySettings.getUnclaimDeleteEntityTypes()); return unclaimDeleteEntityTypes; } + + @ApiStatus.Internal + public @Nullable Collection getUnclaimDeleteEntityTypeNames() { + return this.unclaimDeleteEntityTypes != null ? this.unclaimDeleteEntityTypes.getNames() : null; + } public void setUnclaimDeleteEntityTypes(List entityTypes) { - this.unclaimDeleteEntityTypes = TownySettings.toEntityTypeSet(entityTypes); + this.unclaimDeleteEntityTypes = CompactableCollection.entityTypes(entityTypes); } public void setUsingPlotManagementMayorDelete(boolean using) { @@ -448,13 +454,18 @@ public boolean isUsingPlotManagementRevert() { return isUsingPlotManagementRevert; } + @NotNull public Collection getPlotManagementDeleteIds() { - if (plotManagementDeleteIds == null) return TownySettings.getPlotManagementDeleteIds(); else return plotManagementDeleteIds; } + + @ApiStatus.Internal + public @Nullable Collection getPlotManagementDeleteNames() { + return this.plotManagementDeleteIds != null ? this.plotManagementDeleteIds.getNames() : null; + } public boolean isPlotManagementDeleteIds(Material material) { @@ -462,16 +473,21 @@ public boolean isPlotManagementDeleteIds(Material material) { } public void setPlotManagementDeleteIds(List plotManagementDeleteIds) { - this.plotManagementDeleteIds = new HashSet<>(TownySettings.toMaterialSet(plotManagementDeleteIds)); + this.plotManagementDeleteIds = CompactableCollection.materials(plotManagementDeleteIds); } + @NotNull public Collection getPlotManagementMayorDelete() { - if (plotManagementMayorDelete == null) return TownySettings.getPlotManagementMayorDelete(); else return plotManagementMayorDelete; } + + @ApiStatus.Internal + public @Nullable Collection getPlotManagementMayorDeleteNames() { + return this.plotManagementMayorDelete != null ? this.plotManagementMayorDelete.getNames() : null; + } public boolean isPlotManagementMayorDelete(Material material) { @@ -479,7 +495,7 @@ public boolean isPlotManagementMayorDelete(Material material) { } public void setPlotManagementMayorDelete(List plotManagementMayorDelete) { - this.plotManagementMayorDelete = new HashSet<>(TownySettings.toMaterialSet(plotManagementMayorDelete)); + this.plotManagementMayorDelete = CompactableCollection.materials(plotManagementMayorDelete); } public boolean isUnclaimedBlockAllowedToRevert(Material mat) { @@ -490,31 +506,42 @@ public boolean isUnclaimedBlockAllowedToRevert(Material mat) { return !isPlotManagementIgnoreIds(mat); } + @NotNull public Collection getPlotManagementIgnoreIds() { - if (plotManagementIgnoreIds == null) return TownySettings.getPlotManagementIgnoreIds(); else return plotManagementIgnoreIds; } + + @ApiStatus.Internal + public @Nullable Collection getPlotManagementIgnoreNames() { + return this.plotManagementIgnoreIds != null ? this.plotManagementIgnoreIds.getNames() : null; + } public boolean isPlotManagementIgnoreIds(Material mat) { return getPlotManagementIgnoreIds().contains(mat); } public void setPlotManagementIgnoreIds(List plotManagementIgnoreIds) { - this.plotManagementIgnoreIds = new HashSet<>(TownySettings.toMaterialSet(plotManagementIgnoreIds)); + this.plotManagementIgnoreIds = CompactableCollection.materials(plotManagementIgnoreIds); } + @NotNull public Collection getRevertOnUnclaimWhitelistMaterials() { if (revertOnUnclaimWhitelistMaterials == null) return TownySettings.getRevertOnUnclaimWhitelistMaterials(); else return revertOnUnclaimWhitelistMaterials; } + + @ApiStatus.Internal + public @Nullable Collection getRevertOnUnclaimWhitelistMaterialNames() { + return this.revertOnUnclaimWhitelistMaterials != null ? this.revertOnUnclaimWhitelistMaterials.getNames() : null; + } public void setRevertOnUnclaimWhitelistMaterials(List revertOnUnclaimWhitelistMaterials) { - this.revertOnUnclaimWhitelistMaterials = new HashSet<>(TownySettings.toMaterialSet(revertOnUnclaimWhitelistMaterials)); + this.revertOnUnclaimWhitelistMaterials = CompactableCollection.materials(revertOnUnclaimWhitelistMaterials); } public boolean isRevertOnUnclaimWhitelistMaterial(Material mat) { @@ -572,17 +599,21 @@ public void setPlotManagementWildRevertDelay(long plotManagementWildRevertDelay) } public void setPlotManagementWildRevertEntities(List entities) { - entityExplosionProtection = new HashSet<>(); - entityExplosionProtection.addAll(TownySettings.toEntityTypeSet(entities)); + this.entityExplosionProtection = CompactableCollection.entityTypes(entities); } + @NotNull public Collection getPlotManagementWildRevertEntities() { - if (entityExplosionProtection == null) setPlotManagementWildRevertEntities(TownySettings.getWildExplosionProtectionEntities()); return entityExplosionProtection; } + + @ApiStatus.Internal + public @Nullable Collection getPlotManagementWildRevertEntityNames() { + return this.entityExplosionProtection != null ? this.entityExplosionProtection.getNames() : null; + } public boolean isProtectingExplosionEntity(Entity entity) { @@ -594,17 +625,21 @@ public boolean isProtectingExplosionEntity(Entity entity) { } public void setPlotManagementWildRevertBlockWhitelist(List mats) { - plotManagementWildRevertBlockWhitelist = new HashSet<>(); - plotManagementWildRevertBlockWhitelist.addAll(TownySettings.toMaterialSet(mats)); + plotManagementWildRevertBlockWhitelist = CompactableCollection.materials(mats); } + @NotNull public Collection getPlotManagementWildRevertBlockWhitelist() { - if (plotManagementWildRevertBlockWhitelist == null) setPlotManagementWildRevertBlockWhitelist(TownySettings.getWildExplosionRevertBlockWhitelist()); return plotManagementWildRevertBlockWhitelist; } + + @ApiStatus.Internal + public @Nullable Collection getPlotManagementWildRevertBlockWhitelistNames() { + return this.plotManagementWildRevertBlockWhitelist != null ? this.plotManagementWildRevertBlockWhitelist.getNames() : null; + } public boolean isPlotManagementWildRevertWhitelistedBlock(Material mat) { @@ -622,15 +657,20 @@ public boolean isExplodedBlockAllowedToRevert(Material mat) { } public void setWildRevertMaterialsToNotOverwrite(List mats) { - wildRevertMaterialsToNotOverwrite = new HashSet<>(); - wildRevertMaterialsToNotOverwrite.addAll(TownySettings.toMaterialSet(mats)); + wildRevertMaterialsToNotOverwrite = CompactableCollection.materials(mats); } + @NotNull public Collection getWildRevertMaterialsToNotOverwrite() { if (wildRevertMaterialsToNotOverwrite == null) setWildRevertMaterialsToNotOverwrite(TownySettings.getWildExplosionRevertMaterialsToNotOverwrite()); return wildRevertMaterialsToNotOverwrite; } + + @ApiStatus.Internal + public @Nullable Collection getWildRevertMaterialsToNotOverwriteNames() { + return this.wildRevertMaterialsToNotOverwrite != null ? this.wildRevertMaterialsToNotOverwrite.getNames() : null; + } public boolean isMaterialNotAllowedToBeOverwrittenByWildRevert(Material mat) { if (wildRevertMaterialsToNotOverwrite == null) @@ -650,16 +690,21 @@ public boolean isBlockAllowedToRevert(Material mat) { } public void setPlotManagementWildRevertMaterials(List mats) { - blockExplosionProtection = new HashSet<>(TownySettings.toMaterialSet(mats)); + blockExplosionProtection = CompactableCollection.materials(mats); } + @NotNull public Collection getPlotManagementWildRevertBlocks() { - if (blockExplosionProtection == null) setPlotManagementWildRevertMaterials(TownySettings.getWildExplosionProtectionBlocks()); return blockExplosionProtection; } + + @ApiStatus.Internal + public @Nullable Collection getPlotManagementWildRevertBlockNames() { + return this.blockExplosionProtection != null ? this.blockExplosionProtection.getNames() : null; + } public boolean isProtectingExplosionBlock(Material material) { @@ -672,18 +717,23 @@ public boolean isProtectingExplosionBlock(Material material) { public void setUnclaimedZoneIgnore(List unclaimedZoneIgnoreIds) { if (unclaimedZoneIgnoreIds == null) - this.unclaimedZoneIgnoreBlockMaterials = new HashSet<>(TownySettings.getUnclaimedZoneIgnoreMaterials()); + this.unclaimedZoneIgnoreBlockMaterials = CompactableCollection.materials(TownySettings.getUnclaimedZoneIgnoreMaterials()); else - this.unclaimedZoneIgnoreBlockMaterials = new HashSet<>(TownySettings.toMaterialSet(unclaimedZoneIgnoreIds)); + this.unclaimedZoneIgnoreBlockMaterials = CompactableCollection.materials(unclaimedZoneIgnoreIds); } public Collection getUnclaimedZoneIgnoreMaterials() { if (unclaimedZoneIgnoreBlockMaterials == null) - return TownySettings.getUnclaimedZoneIgnoreMaterials(); + return TownySettings.toMaterialSet(TownySettings.getUnclaimedZoneIgnoreMaterials()); else return unclaimedZoneIgnoreBlockMaterials; } + + @ApiStatus.Internal // Intended for saving to database + public @Nullable Collection getUnclaimedZoneIgnoreMaterialNames() { + return this.unclaimedZoneIgnoreBlockMaterials != null ? this.unclaimedZoneIgnoreBlockMaterials.getNames() : null; + } public boolean isUnclaimedZoneIgnoreMaterial(Material mat) { diff --git a/Towny/src/main/java/com/palmergames/bukkit/util/BukkitTools.java b/Towny/src/main/java/com/palmergames/bukkit/util/BukkitTools.java index 5b699a92aad..f95138ca731 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/util/BukkitTools.java +++ b/Towny/src/main/java/com/palmergames/bukkit/util/BukkitTools.java @@ -19,6 +19,7 @@ import org.bukkit.World; import org.bukkit.command.CommandMap; import org.bukkit.command.CommandSender; +import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; @@ -40,9 +41,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -423,14 +425,14 @@ public static String keyAsString(@NotNull NamespacedKey key) { @ApiStatus.Internal public static Collection convertKeyedToString(@NotNull Collection keys) { - final Set set = new HashSet<>(); + final Set set = new LinkedHashSet<>(); for (Keyed keyed : keys) set.add(keyAsString(keyed.getKey())); return set; } - + static { MethodHandle temp = null; try { diff --git a/Towny/src/main/java/com/palmergames/bukkit/util/EntityLists.java b/Towny/src/main/java/com/palmergames/bukkit/util/EntityLists.java index 72206b7f832..1900a7ca976 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/util/EntityLists.java +++ b/Towny/src/main/java/com/palmergames/bukkit/util/EntityLists.java @@ -1,13 +1,25 @@ package com.palmergames.bukkit.util; +import com.google.common.collect.ImmutableSet; import com.palmergames.bukkit.towny.object.AbstractRegistryList; +import org.bukkit.Keyed; import org.bukkit.Registry; import org.bukkit.entity.Animals; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Monster; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; +import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; public class EntityLists extends AbstractRegistryList { @@ -45,9 +57,52 @@ public boolean contains(@NotNull Entity entity) { public static final EntityLists PVP_EXPLOSIVE = newBuilder().add("firework_rocket", "tnt_minecart", "tnt", "end_crystal").build(); - public static final EntityLists ANIMALS = newBuilder().filter(type -> type.getEntityClass() != null && Animals.class.isAssignableFrom(type.getEntityClass())).build(); + public static final EntityLists ANIMALS = newBuilder().addIf(type -> type.getEntityClass() != null && Animals.class.isAssignableFrom(type.getEntityClass())).build(); + + public static final EntityLists MONSTERS = newBuilder().addIf(type -> type.getEntityClass() != null && Monster.class.isAssignableFrom(type.getEntityClass())).build(); + + private static final Map GROUPS = Arrays.stream(EntityLists.class.getFields()) + .filter(field -> Modifier.isStatic(field.getModifiers())) + .filter(field -> field.getType().equals(EntityLists.class)) + .collect(Collectors.toMap(field -> field.getName().toLowerCase(Locale.ROOT), field -> { + try { + return (EntityLists) field.get(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + })); + + private static final Set CUSTOM_GROUPS = new HashSet<>(); + + @NotNull + @Unmodifiable + public static Set getGrouping(@NotNull String groupName) { + final EntityLists grouping = GROUPS.get(groupName.toLowerCase(Locale.ROOT)); + + return grouping != null ? ImmutableSet.copyOf(grouping.tagged) : ImmutableSet.of(); + } + + public static boolean hasGroup(@NotNull String groupName) { + return GROUPS.containsKey(groupName.toLowerCase(Locale.ROOT)); + } + + public static void addGroup(@NotNull String groupName, @NotNull EntityLists group) { + GROUPS.put(groupName.toLowerCase(Locale.ROOT), group); + CUSTOM_GROUPS.add(groupName.toLowerCase(Locale.ROOT)); + } + + @ApiStatus.Internal + public static void clearCustomGroups() { + CUSTOM_GROUPS.forEach(GROUPS::remove); + CUSTOM_GROUPS.clear(); + } + + @ApiStatus.Internal + public static Map> allGroups() { + return GROUPS.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().tagged())); + } public static Builder newBuilder() { - return new Builder<>(Registry.ENTITY_TYPE, EntityType.class, EntityLists::new).filter(type -> type != EntityType.UNKNOWN); + return new Builder<>(Registry.ENTITY_TYPE, EntityType.class, EntityLists::new); } } diff --git a/Towny/src/main/java/com/palmergames/bukkit/util/ItemLists.java b/Towny/src/main/java/com/palmergames/bukkit/util/ItemLists.java index 24ac3876a71..e258b72142e 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/util/ItemLists.java +++ b/Towny/src/main/java/com/palmergames/bukkit/util/ItemLists.java @@ -1,19 +1,22 @@ package com.palmergames.bukkit.util; -import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collection; -import java.util.List; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import com.google.common.collect.ImmutableSet; import com.palmergames.bukkit.towny.object.AbstractRegistryList; +import org.bukkit.Keyed; import org.bukkit.Material; import org.bukkit.Registry; import org.bukkit.Tag; import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.ApiStatus.Internal; @@ -304,28 +307,54 @@ public Collection getMaterialNameCollection() { /** * Config-useable material groups. */ - public static final Set GROUPS = Arrays.stream(ItemLists.class.getFields()).filter(field -> Modifier.isStatic(field.getModifiers())).map(Field::getName).filter(name -> !name.equals("GROUPS")).collect(Collectors.toSet()); + private static final Map GROUPS = Arrays.stream(ItemLists.class.getFields()) + .filter(field -> Modifier.isStatic(field.getModifiers())) + .filter(field -> field.getType().equals(ItemLists.class)) + .collect(Collectors.toMap(field -> field.getName().toLowerCase(Locale.ROOT), field -> { + try { + return (ItemLists) field.get(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + })); + + private static final Set CUSTOM_GROUPS = new HashSet<>(); /** * Returns a pre-configured list from the GROUPS. * - * @param groupName - String value of one of the {@link ItemLists#GROUPS} - * @return - Set<Material> grouping of materials, or an empty set if the grouping was not found. + * @param groupName String value of one of the {@link ItemLists#GROUPS} + * @return Set<Material> grouping of materials, or an empty set if the grouping was not found. */ @NotNull @Unmodifiable - public static Set getGrouping(String groupName) { - if (!GROUPS.contains(groupName)) - return ImmutableSet.of(); - - try { - return ImmutableSet.copyOf(((ItemLists) ItemLists.class.getField(groupName).get(null)).tagged); - } catch (Exception e) { - return ImmutableSet.of(); - } + public static Set getGrouping(@NotNull String groupName) { + final ItemLists grouping = GROUPS.get(groupName.toLowerCase(Locale.ROOT)); + + return grouping != null ? ImmutableSet.copyOf(grouping.tagged) : ImmutableSet.of(); + } + + public static boolean hasGroup(@NotNull String groupName) { + return GROUPS.containsKey(groupName.toLowerCase(Locale.ROOT)); + } + + public static void addGroup(@NotNull String groupName, @NotNull ItemLists group) { + GROUPS.put(groupName.toLowerCase(Locale.ROOT), group); + CUSTOM_GROUPS.add(groupName.toLowerCase(Locale.ROOT)); + } + + @ApiStatus.Internal + public static void clearCustomGroups() { + CUSTOM_GROUPS.forEach(GROUPS::remove); + CUSTOM_GROUPS.clear(); + } + + @Internal + public static Map> allGroups() { + return GROUPS.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().tagged())); } public static Builder newBuilder() { - return new Builder<>(Registry.MATERIAL, Material.class, ItemLists::new).notStartsWith("LEGACY_"); + return new Builder<>(Registry.MATERIAL, Material.class, ItemLists::new); } } diff --git a/Towny/src/test/java/com/palmergames/bukkit/towny/object/RegistryListTests.java b/Towny/src/test/java/com/palmergames/bukkit/towny/object/RegistryListTests.java index b1a0a040dd6..6da53799990 100644 --- a/Towny/src/test/java/com/palmergames/bukkit/towny/object/RegistryListTests.java +++ b/Towny/src/test/java/com/palmergames/bukkit/towny/object/RegistryListTests.java @@ -2,12 +2,24 @@ import be.seeseemelk.mockbukkit.MockBukkit; import com.palmergames.bukkit.towny.TownySettings; +import com.palmergames.bukkit.util.EntityLists; import com.palmergames.bukkit.util.ItemLists; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.entity.EntityType; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.bukkit.Material.*; import static org.junit.jupiter.api.Assertions.*; +@SuppressWarnings("ArraysAsListWithZeroOrOneArgument") public class RegistryListTests { @BeforeAll @@ -23,13 +35,69 @@ void testListAdd() { } @Test - void testMissingListAddDoesntAddEverything() { - ItemLists itemList = ItemLists.newBuilder().add("materialthatdefinitelyexists").build(); - assertEquals(0, itemList.tagged().size()); + void testCustomRegistryListBuilderClassNames() { + EntityLists built = TownySettings.constructRegistryList(EntityLists.newBuilder(), Tag.REGISTRY_ENTITY_TYPES, Arrays.asList("c:Animals"), EntityType::getEntityClass); + assertEquals(EntityLists.ANIMALS.tagged(), built.tagged()); + + Set expected = new HashSet<>(Set.of(WEEPING_VINES, NETHER_WART, FROSTED_ICE, SWEET_BERRY_BUSH, CACTUS, COCOA, CAVE_VINES, PITCHER_CROP, BEETROOTS, SUGAR_CANE, CHORUS_FLOWER, FIRE, BAMBOO, POTATOES, MELON_STEM, CARROTS, TORCHFLOWER_CROP, WHEAT, MANGROVE_PROPAGULE, KELP, TWISTING_VINES, PUMPKIN_STEM)); + List input = new ArrayList<>(Arrays.asList("c:org.bukkit.block.data.Ageable")); + + ItemLists list = TownySettings.constructRegistryList(ItemLists.newBuilder(), Tag.REGISTRY_BLOCKS, input, type -> type.data); + assertEquals(expected, list.tagged()); + + expected.remove(WEEPING_VINES); + input.add("-weeping_vines"); + list = TownySettings.constructRegistryList(ItemLists.newBuilder(), Tag.REGISTRY_BLOCKS, input, type -> type.data); + assertEquals(expected, list.tagged()); + } + + @Test + void testCompactableCollectionRemoval() { + AbstractRegistryList.CompactableCollection collection = new AbstractRegistryList.CompactableCollection<>(EntityType.class); + collection.setNames(List.of("wolf", "monsters")); + + assertTrue(collection.contains(EntityType.WOLF)); + assertTrue(collection.contains(EntityType.ZOMBIE)); + + collection.remove(EntityType.ZOMBIE); + assertFalse(collection.contains(EntityType.ZOMBIE)); + + // Zombie is removed now, so the group gets unwrapped and the size of the backing names must increase + assertTrue(collection.getNames().size() > 10); + + // Test compaction + assertTrue(collection.add(EntityType.ZOMBIE)); + assertTrue(collection.compact()); + + assertEquals(2, collection.getNames().size()); + + assertFalse(collection.compact()); // Nothing to compact + } + + @Test + void testCompactableCollectionCompact() { + AbstractRegistryList.CompactableCollection collection = AbstractRegistryList.CompactableCollection.entityTypes(); + collection.setNames(List.of("monsters")); + + collection.remove(EntityType.ZOMBIE); + + assertTrue(collection.getNames().size() > 10); + + collection.add(EntityType.ZOMBIE); + + assertTrue(collection.getNames().size() > 10); + assertTrue(collection.compact()); + + assertEquals(1, collection.getNames().size()); + assertFalse(collection.compact()); } @Test - void testNothingAddedToTheBuilderGivesEverything() { - assertTrue(ItemLists.newBuilder().build().tagged().size() > 100); + void testCompactableDuplicateRemoval() { + AbstractRegistryList.CompactableCollection collection = AbstractRegistryList.CompactableCollection.entityTypes(); + collection.setNames(List.of("monsters", "zombie")); + + assertTrue(collection.remove(EntityType.ZOMBIE)); + assertTrue(collection.getNames().size() > 10); } }