diff --git a/pom.xml b/pom.xml
index 8555e1e10..17163bc3e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -309,7 +309,9 @@
wiki.json
languages/translators.json
+
tags/*.json
+ biome-maps/*.json
languages/**/*.yml
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/api/exceptions/BiomeMapException.java b/src/main/java/io/github/thebusybiscuit/slimefun4/api/exceptions/BiomeMapException.java
new file mode 100644
index 000000000..9e691185a
--- /dev/null
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/api/exceptions/BiomeMapException.java
@@ -0,0 +1,48 @@
+package io.github.thebusybiscuit.slimefun4.api.exceptions;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.bukkit.NamespacedKey;
+
+import io.github.thebusybiscuit.slimefun4.utils.biomes.BiomeMap;
+
+/**
+ * An {@link BiomeMapException} is thrown whenever a {@link BiomeMap}
+ * contains illegal, invalid or unknown values.
+ *
+ * @author TheBusyBiscuit
+ *
+ */
+public class BiomeMapException extends Exception {
+
+ private static final long serialVersionUID = -1894334121194788527L;
+
+ /**
+ * This constructs a new {@link BiomeMapException} for the given
+ * {@link BiomeMap}'s {@link NamespacedKey} with the provided context.
+ *
+ * @param key
+ * The {@link NamespacedKey} of our {@link BiomeMap}
+ * @param message
+ * The message to display
+ */
+ @ParametersAreNonnullByDefault
+ public BiomeMapException(NamespacedKey key, String message) {
+ super("Biome Map '" + key + "' has been misconfigured: " + message);
+ }
+
+ /**
+ * This constructs a new {@link BiomeMapException} for the given
+ * {@link BiomeMap}'s {@link NamespacedKey} with the provided context.
+ *
+ * @param key
+ * The {@link NamespacedKey} of our {@link BiomeMap}
+ * @param cause
+ * The {@link Throwable} which has caused this to happen
+ */
+ @ParametersAreNonnullByDefault
+ public BiomeMapException(NamespacedKey key, Throwable cause) {
+ super("Tag '" + key + "' has been misconfigured (" + cause.getMessage() + ')', cause);
+ }
+
+}
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/resources/SlimefunResource.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/resources/SlimefunResource.java
index b8fb6c7e2..975e463df 100644
--- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/resources/SlimefunResource.java
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/resources/SlimefunResource.java
@@ -1,5 +1,7 @@
package io.github.thebusybiscuit.slimefun4.implementation.resources;
+import java.util.logging.Level;
+
import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;
@@ -7,8 +9,12 @@ import org.apache.commons.lang.Validate;
import org.bukkit.NamespacedKey;
import org.bukkit.inventory.ItemStack;
+import com.google.gson.JsonElement;
+
+import io.github.thebusybiscuit.slimefun4.api.exceptions.BiomeMapException;
import io.github.thebusybiscuit.slimefun4.api.geo.GEOResource;
import io.github.thebusybiscuit.slimefun4.implementation.Slimefun;
+import io.github.thebusybiscuit.slimefun4.utils.biomes.BiomeMap;
/**
* This is an abstract parent class for any {@link GEOResource}
@@ -70,4 +76,34 @@ abstract class SlimefunResource implements GEOResource {
return geoMiner;
}
+ /**
+ * Internal helper method for reading a {@link BiomeMap} of {@link Integer} type values from
+ * a resource file.
+ *
+ * @param resource
+ * The {@link SlimefunResource} instance
+ * @param path
+ * The path to our biome map file
+ *
+ * @return A {@link BiomeMap} for this resource
+ */
+ @ParametersAreNonnullByDefault
+ static final @Nonnull BiomeMap getBiomeMap(SlimefunResource resource, String path) {
+ Validate.notNull(resource, "Resource cannot be null");
+ Validate.notNull(path, "Path cannot be null");
+
+ try {
+ return BiomeMap.fromResource(resource.getKey(), path, JsonElement::getAsInt);
+ } catch (BiomeMapException x) {
+ if (Slimefun.instance().isUnitTest()) {
+ // Unit Tests should always fail here, so we re-throw the exception
+ throw new IllegalStateException(x);
+ } else {
+ // In a server environment, we should just print a warning and carry on
+ Slimefun.logger().log(Level.WARNING, x, () -> "Failed to load BiomeMap for GEO-resource: " + resource.getKey());
+ return new BiomeMap<>(resource.getKey());
+ }
+ }
+ }
+
}
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/PatternUtils.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/PatternUtils.java
index 354aa17f5..3063f55f1 100644
--- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/PatternUtils.java
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/PatternUtils.java
@@ -20,7 +20,8 @@ public final class PatternUtils {
public static final Pattern SLASH_SEPARATOR = Pattern.compile(" / ");
- public static final Pattern MINECRAFT_MATERIAL = Pattern.compile("minecraft:[a-z_]+");
+ public static final Pattern MINECRAFT_NAMESPACEDKEY = Pattern.compile("minecraft:[a-z_]+");
+
public static final Pattern MINECRAFT_TAG = Pattern.compile("#minecraft:[a-z_]+");
public static final Pattern SLIMEFUN_TAG = Pattern.compile("#slimefun:[a-z_]+");
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMap.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMap.java
new file mode 100644
index 000000000..60c8488a9
--- /dev/null
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMap.java
@@ -0,0 +1,107 @@
+package io.github.thebusybiscuit.slimefun4.utils.biomes;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.apache.commons.lang.Validate;
+import org.bukkit.Keyed;
+import org.bukkit.NamespacedKey;
+import org.bukkit.block.Biome;
+
+import com.google.gson.JsonElement;
+
+import io.github.thebusybiscuit.slimefun4.api.exceptions.BiomeMapException;
+import io.github.thebusybiscuit.slimefun4.implementation.Slimefun;
+
+public class BiomeMap implements Keyed {
+
+ private final Map dataMap = new EnumMap<>(Biome.class);
+ private final NamespacedKey namespacedKey;
+
+ @ParametersAreNonnullByDefault
+ public BiomeMap(NamespacedKey namespacedKey) {
+ Validate.notNull(namespacedKey, "The key must not be null.");
+
+ this.namespacedKey = namespacedKey;
+ }
+
+ public @Nullable T get(@Nonnull Biome biome) {
+ Validate.notNull(biome, "The biome shall not be null.");
+
+ return dataMap.get(biome);
+ }
+
+ public @Nonnull T getOrDefault(@Nonnull Biome biome, T defaultValue) {
+ Validate.notNull(biome, "The biome should not be null.");
+
+ return dataMap.getOrDefault(biome, defaultValue);
+ }
+
+ public boolean contains(@Nonnull Biome biome) {
+ Validate.notNull(biome, "The biome must not be null.");
+
+ return dataMap.containsKey(biome);
+ }
+
+ public boolean put(@Nonnull Biome biome, @Nonnull T value) {
+ Validate.notNull(biome, "The biome should not be null.");
+ Validate.notNull(value, "Values cannot be null.");
+
+ return dataMap.put(biome, value) == null;
+ }
+
+ public void putAll(@Nonnull Map map) {
+ Validate.notNull(map, "The map should not be null.");
+
+ dataMap.putAll(map);
+ }
+
+ public void putAll(@Nonnull BiomeMap map) {
+ Validate.notNull(map, "The map should not be null.");
+
+ dataMap.putAll(map.dataMap);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public @Nonnull NamespacedKey getKey() {
+ return namespacedKey;
+ }
+
+ @Override
+ public String toString() {
+ return "BiomeMap " + dataMap.toString();
+ }
+
+ @ParametersAreNonnullByDefault
+ public static @Nonnull BiomeMap fromJson(NamespacedKey key, String json, Function valueConverter) throws BiomeMapException {
+ // All parameters are validated by the Parser.
+ BiomeMapParser parser = new BiomeMapParser<>(key, valueConverter);
+ parser.read(json);
+ return parser.buildBiomeMap();
+ }
+
+ @ParametersAreNonnullByDefault
+ public static @Nonnull BiomeMap fromResource(NamespacedKey key, String path, Function valueConverter) throws BiomeMapException {
+ Validate.notNull(key, "The key shall not be null.");
+ Validate.notNull(path, "The path should not be null!");
+
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(Slimefun.class.getResourceAsStream(path), StandardCharsets.UTF_8))) {
+ return fromJson(key, reader.lines().collect(Collectors.joining("")), valueConverter);
+ } catch (IOException x) {
+ throw new BiomeMapException(key, x);
+ }
+ }
+}
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMapParser.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMapParser.java
new file mode 100644
index 000000000..93a2ab87e
--- /dev/null
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/BiomeMapParser.java
@@ -0,0 +1,135 @@
+package io.github.thebusybiscuit.slimefun4.utils.biomes;
+
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.apache.commons.lang.Validate;
+import org.bukkit.NamespacedKey;
+import org.bukkit.block.Biome;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+
+import io.github.bakedlibs.dough.common.CommonPatterns;
+import io.github.thebusybiscuit.slimefun4.api.exceptions.BiomeMapException;
+import io.github.thebusybiscuit.slimefun4.utils.PatternUtils;
+
+class BiomeMapParser {
+
+ private static final String VALUE_KEY = "value";
+ private static final String BIOMES_KEY = "biomes";
+
+ private final NamespacedKey key;
+ private final Function valueConverter;
+ private final Map map = new EnumMap<>(Biome.class);
+
+ @ParametersAreNonnullByDefault
+ BiomeMapParser(NamespacedKey key, Function valueConverter) {
+ Validate.notNull(key, "The key shall not be null.");
+ Validate.notNull(valueConverter, "You must provide a Function to convert raw json values to your desired data type.");
+
+ this.key = key;
+ this.valueConverter = valueConverter;
+ }
+
+ void read(@Nonnull String json) throws BiomeMapException {
+ Validate.notNull(json, "The JSON string should not be null!");
+ JsonArray root = null;
+
+ try {
+ JsonParser parser = new JsonParser();
+ root = parser.parse(json).getAsJsonArray();
+ } catch (IllegalStateException | JsonParseException x) {
+ throw new BiomeMapException(key, x);
+ }
+
+ /*
+ * We don't include this in our try/catch, as this type of exception
+ * is already specified in the throws-declaration.
+ */
+ read(root);
+ }
+
+ void read(@Nonnull JsonArray json) throws BiomeMapException {
+ Validate.notNull(json, "The JSON Array should not be null!");
+
+ for (JsonElement element : json) {
+ if (element instanceof JsonObject) {
+ readEntry(element.getAsJsonObject());
+ } else {
+ throw new BiomeMapException(key, "Unexpected array element: " + element.getClass().getSimpleName() + " - " + element.toString());
+ }
+ }
+ }
+
+ private void readEntry(@Nonnull JsonObject entry) throws BiomeMapException {
+ Validate.notNull(entry, "The JSON entry should not be null!");
+
+ if (entry.has(VALUE_KEY)) {
+ T value = valueConverter.apply(entry.get(VALUE_KEY));
+
+ if (entry.has(BIOMES_KEY) && entry.get(BIOMES_KEY).isJsonArray()) {
+ Set biomes = readBiomes(entry.get(BIOMES_KEY).getAsJsonArray());
+
+ for (Biome biome : biomes) {
+ T prev = map.put(biome, value);
+
+ if (prev != null) {
+ throw new BiomeMapException(key, "Biome '" + biome.getKey() + "' is registered twice");
+ }
+ }
+ } else {
+ throw new BiomeMapException(key, "Entry is missing a 'biomes' child of type array.");
+ }
+ } else {
+ throw new BiomeMapException(key, "Entry is missing a 'value' child.");
+ }
+ }
+
+ private @Nonnull Set readBiomes(@Nonnull JsonArray array) throws BiomeMapException {
+ Validate.notNull(array, "The JSON array should not be null!");
+ Set biomes = EnumSet.noneOf(Biome.class);
+
+ for (JsonElement element : array) {
+ if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) {
+ String value = element.getAsString();
+
+ if (PatternUtils.MINECRAFT_NAMESPACEDKEY.matcher(value).matches()) {
+ String formattedValue = CommonPatterns.COLON.split(value)[1].toUpperCase(Locale.ROOT);
+
+ try {
+ Biome biome = Biome.valueOf(formattedValue);
+ biomes.add(biome);
+ } catch (IllegalArgumentException x) {
+ throw new BiomeMapException(key, "The Biome '" + value + "' does not exist!");
+ }
+ } else {
+ // The regular expression did not match
+ throw new BiomeMapException(key, "Could not recognize value '" + value + "'");
+ }
+ } else {
+ throw new BiomeMapException(key, "Unexpected array element: " + element.getClass().getSimpleName() + " - " + element.toString());
+ }
+ }
+
+ return biomes;
+ }
+
+ @Nonnull
+ BiomeMap buildBiomeMap() {
+ BiomeMap biomeMap = new BiomeMap<>(key);
+ biomeMap.putAll(map);
+ return biomeMap;
+ }
+
+}
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/package-info.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/package-info.java
new file mode 100644
index 000000000..c4f513a0b
--- /dev/null
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/biomes/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains classes centered around our {@link io.github.thebusybiscuit.slimefun4.utils.biomes.BiomeMap}
+ * utility.
+ */
+package io.github.thebusybiscuit.slimefun4.utils.biomes;
\ No newline at end of file
diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/tags/TagParser.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/tags/TagParser.java
index 41d951452..dc181d2d0 100644
--- a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/tags/TagParser.java
+++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/tags/TagParser.java
@@ -134,7 +134,7 @@ public class TagParser implements Keyed {
@ParametersAreNonnullByDefault
private void parsePrimitiveValue(String value, Set materials, Set> tags, boolean throwException) throws TagMisconfigurationException {
- if (PatternUtils.MINECRAFT_MATERIAL.matcher(value).matches()) {
+ if (PatternUtils.MINECRAFT_NAMESPACEDKEY.matcher(value).matches()) {
// Match the NamespacedKey against Materials
Material material = Material.matchMaterial(value);
diff --git a/src/test/java/io/github/thebusybiscuit/slimefun4/utils/biomes/TestBiomeMapParser.java b/src/test/java/io/github/thebusybiscuit/slimefun4/utils/biomes/TestBiomeMapParser.java
new file mode 100644
index 000000000..aaa436432
--- /dev/null
+++ b/src/test/java/io/github/thebusybiscuit/slimefun4/utils/biomes/TestBiomeMapParser.java
@@ -0,0 +1,78 @@
+package io.github.thebusybiscuit.slimefun4.utils.biomes;
+
+import java.util.function.Function;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import org.bukkit.NamespacedKey;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import com.google.gson.JsonElement;
+
+import io.github.thebusybiscuit.slimefun4.api.exceptions.BiomeMapException;
+import io.github.thebusybiscuit.slimefun4.implementation.Slimefun;
+
+import be.seeseemelk.mockbukkit.MockBukkit;
+
+class TestBiomeMapParser {
+
+ private static final Function AS_STRING = JsonElement::getAsString;
+ private static final Function AS_INT = JsonElement::getAsInt;
+
+ private static Slimefun plugin;
+ private static NamespacedKey key;
+
+ @BeforeAll
+ public static void load() {
+ MockBukkit.mock();
+ plugin = MockBukkit.load(Slimefun.class);
+ key = new NamespacedKey(plugin, "test");
+ }
+
+ @AfterAll
+ public static void unload() {
+ MockBukkit.unmock();
+ }
+
+ @Test
+ @DisplayName("Test JSON Parsing Error handling")
+ void testInvalidJson() {
+ assertMisconfiguration(AS_STRING, "");
+ assertMisconfiguration(AS_STRING, "1234");
+ assertMisconfiguration(AS_STRING, "hello world");
+ assertMisconfiguration(AS_STRING, "{}");
+ }
+
+ @Test
+ @DisplayName("Test Array not having proper children")
+ void testInvalidArrayChildren() {
+ assertMisconfiguration(AS_STRING, "[1, 2, 3]");
+ assertMisconfiguration(AS_STRING, "[[1], [2]]");
+ assertMisconfiguration(AS_STRING, "[\"foo\", \"bar\"]");
+ }
+
+ @Test
+ @DisplayName("Test Array entries being misconfigured")
+ void testInvalidEntries() {
+ assertMisconfiguration(AS_STRING, "[{}]");
+ assertMisconfiguration(AS_STRING, "[{\"id\": \"one\"}]");
+ }
+
+ @Test
+ @DisplayName("Test Array entries being incomplete")
+ void testIncompleteEntries() {
+ assertMisconfiguration(AS_STRING, "[{\"value\": \"cool\"}]");
+ assertMisconfiguration(AS_STRING, "[{\"biomes\": []}]");
+ }
+
+ @ParametersAreNonnullByDefault
+ private void assertMisconfiguration(Function function, String json) {
+ BiomeMapParser parser = new BiomeMapParser<>(key, function);
+ Assertions.assertThrows(BiomeMapException.class, () -> parser.read(json));
+ }
+
+}