Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to define their own item/entity lists #6865

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
61 changes: 52 additions & 9 deletions Towny/src/main/java/com/palmergames/bukkit/config/ConfigNodes.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.palmergames.bukkit.config;

import java.util.HashMap;

public enum ConfigNodes {
VERSION_HEADER("version", "", ""),
VERSION(
Expand Down Expand Up @@ -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"),
Comment on lines +3270 to +3284
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ought to be boxed in with a header like the other config sections, if the header is a bit wider than they are normally that's fine.

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;
}

Expand All @@ -3283,7 +3317,7 @@ public enum ConfigNodes {
*/
public String getRoot() {

return Root;
return root;
}

/**
Expand All @@ -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;
}

/**
Expand Down
177 changes: 144 additions & 33 deletions Towny/src/main/java/com/palmergames/bukkit/towny/TownySettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -101,8 +109,8 @@ public record NationLevel(
private static final SortedMap<Integer, TownLevel> configTownLevel = Collections.synchronizedSortedMap(new TreeMap<>(Collections.reverseOrder()));
private static final SortedMap<Integer, NationLevel> configNationLevel = Collections.synchronizedSortedMap(new TreeMap<>(Collections.reverseOrder()));

private static final Set<Material> itemUseMaterials = new HashSet<>();
private static final Set<Material> switchUseMaterials = new HashSet<>();
private static final Set<Material> itemUseMaterials = new LinkedHashSet<>();
private static final Set<Material> switchUseMaterials = new LinkedHashSet<>();
private static final List<Class<?>> protectedMobs = new ArrayList<>();

private static final Map<NamespacedKey, Consumer<CommentedConfiguration>> CONFIG_RELOAD_LISTENERS = new HashMap<>();
Expand Down Expand Up @@ -453,6 +461,7 @@ public static void loadConfig(Path configPath, String version) {

config.save();

loadCustomRegistryLists();
loadSwitchAndItemUseMaterialsLists();
loadProtectedMobsList();
ChunkNotification.loadFormatStrings();
Expand Down Expand Up @@ -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<String> 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<String> 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<EntityType> toEntityTypeSet$$bridge$$public(final List<String> entityList) {
return toEntityTypeSet(entityList);
}

public static Set<EntityType> toEntityTypeSet(final List<String> entityList) {
final Set<EntityType> entities = new HashSet<>();
public static Set<EntityType> toEntityTypeSet(final Collection<String> entityList) {
final Set<EntityType> 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.
//<editor-fold desc="Lots of switch cases">
Expand Down Expand Up @@ -553,15 +554,20 @@ public static Set<EntityType> toEntityTypeSet(final List<String> entityList) {
return entities;
}

public static Collection<Material> toMaterialSet(List<String> materialList) {
Set<Material> materials = new HashSet<>();
@Deprecated
private static Collection<Material> toMaterialSet$$bridge$$public(List<String> materialList) {
return toMaterialSet(materialList);
}

public static Collection<Material> toMaterialSet(Collection<String> materialList) {
Set<Material> 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)
Expand Down Expand Up @@ -674,6 +680,19 @@ public static long getMillis(ConfigNodes node) {
return 1;
}
}

public static Map<String, String> getMap(ConfigNodes node) {
final Map<String, String> 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) {

Expand Down Expand Up @@ -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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my experience handling maps gets a bit weird, this is likely the point where townblocktypes stopped working. I ran into something similar in TownyChat when changing the channels.yml over to commented config.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's because setNewProperty used to always toString the value, but this is removed here

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());

}

Expand Down Expand Up @@ -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<String, String> 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<String, String> 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 <T extends Keyed, F extends AbstractRegistryList<T>> F constructRegistryList(final AbstractRegistryList.Builder<T, F> builder, final String registryName, final Iterable<String> elements, final Function<T, Class<?>> 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 <T extends Keyed, F extends AbstractRegistryList<T>> boolean checkClass(AbstractRegistryList.Builder<T, F> builder, Function<T, Class<?>> classExtractor, String className, boolean add) {
final Class<?> desired;
try {
desired = Class.forName(className);
} catch (ClassNotFoundException e) {
return false;
}

Predicate<T> 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<String> 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<String> farmMaterials = new HashSet<>();
farmMaterials.addAll(ItemLists.SAPLINGS.getMaterialNameCollection());
Expand Down Expand Up @@ -1827,9 +1938,9 @@ public static boolean isFireSpreadBypassMaterial(String mat) {
return getFireSpreadBypassMaterials().contains(mat);
}

public static Collection<Material> getUnclaimedZoneIgnoreMaterials() {
public static Collection<String> getUnclaimedZoneIgnoreMaterials() {

return toMaterialSet(getStrArr(ConfigNodes.UNCLAIMED_ZONE_IGNORE));
return getStrArr(ConfigNodes.UNCLAIMED_ZONE_IGNORE);
}

public static List<Class<?>> getProtectedEntityTypes() {
Expand All @@ -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) {
Expand Down
Loading
Loading