diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index 17e4df539..0b954bc49 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -6,6 +6,7 @@ import net.minestom.server.data.DataManager; import net.minestom.server.entity.EntityManager; import net.minestom.server.entity.Player; import net.minestom.server.gamedata.loottables.LootTableManager; +import net.minestom.server.gamedata.tags.TagManager; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.block.BlockManager; import net.minestom.server.listener.manager.PacketListenerManager; @@ -91,6 +92,7 @@ public class MinecraftServer { private static ResponseDataConsumer responseDataConsumer; private static Difficulty difficulty = Difficulty.NORMAL; private static LootTableManager lootTableManager; + private static TagManager tagManager; public static MinecraftServer init() { connectionManager = new ConnectionManager(); @@ -111,6 +113,7 @@ public class MinecraftServer { updateManager = new UpdateManager(); lootTableManager = new LootTableManager(); + tagManager = new TagManager(); nettyServer = new NettyServer(packetProcessor); @@ -208,6 +211,10 @@ public class MinecraftServer { return lootTableManager; } + public static TagManager getTagManager() { + return tagManager; + } + public void start(String address, int port, ResponseDataConsumer responseDataConsumer) { LOGGER.info("Starting Minestom server."); MinecraftServer.responseDataConsumer = responseDataConsumer; diff --git a/src/main/java/net/minestom/server/gamedata/tags/Tag.java b/src/main/java/net/minestom/server/gamedata/tags/Tag.java new file mode 100644 index 000000000..c95a2b677 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/tags/Tag.java @@ -0,0 +1,73 @@ +package net.minestom.server.gamedata.tags; + +import net.minestom.server.utils.NamespaceID; + +import java.io.FileNotFoundException; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represents a group of items, blocks, fluids, entity types or function. + * Immutable by design + */ +public class Tag { + + public static final Tag EMPTY = new Tag(); + + private Set values; + + /** + * Creates a new empty tag + */ + public Tag() { + values = new HashSet<>(); + lockValues(); + } + + /** + * Creates a new tag with the contents of the container + * @param manager Used to load tag contents (as tags are valid values inside 'values') + * @param lowerPriority Tag contents from lower priority data packs. If 'replace' is false in 'container', + * appends the contents of that pack to the one being constructed + * @param container + */ + public Tag(TagManager manager, String type, Tag lowerPriority, TagContainer container) throws FileNotFoundException { + values = new HashSet<>(); + if(!container.replace) { + values.addAll(lowerPriority.values); + } + Objects.requireNonNull(container.values, "Attempted to load from a TagContainer with no 'values' array"); + for(String line : container.values) { + if(line.startsWith("#")) { // pull contents from a tag + Tag subtag = manager.load(NamespaceID.from(line.substring(1)), type); + values.addAll(subtag.values); + } else { + values.add(NamespaceID.from(line)); + } + } + + lockValues(); + } + + private void lockValues() { + values = Set.copyOf(values); + } + + /** + * Checks whether the given id in inside this tag + * @param id the id to check against + * @return 'true' iif this tag contains the given id + */ + public boolean contains(NamespaceID id) { + return values.contains(id); + } + + /** + * Returns an immutable set of values present in this tag + * @return immutable set of values present in this tag + */ + public Set getValues() { + return values; + } +} diff --git a/src/main/java/net/minestom/server/gamedata/tags/TagContainer.java b/src/main/java/net/minestom/server/gamedata/tags/TagContainer.java new file mode 100644 index 000000000..7f181851a --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/tags/TagContainer.java @@ -0,0 +1,9 @@ +package net.minestom.server.gamedata.tags; + +/** + * Meant only for parsing tag JSON + */ +public class TagContainer { + boolean replace; + String[] values; +} diff --git a/src/main/java/net/minestom/server/gamedata/tags/TagManager.java b/src/main/java/net/minestom/server/gamedata/tags/TagManager.java new file mode 100644 index 000000000..28269854f --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/tags/TagManager.java @@ -0,0 +1,102 @@ +package net.minestom.server.gamedata.tags; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.minestom.server.registry.ResourceGatherer; +import net.minestom.server.utils.NamespaceID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.util.HashMap; +import java.util.Map; + +/** + * Handles loading and caching of tags + */ +public class TagManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(TagManager.class); + private final Gson gson; + private Map cache = new HashMap<>(); + + public TagManager() { + gson = new GsonBuilder() + .create(); + } + + /** + * Loads a tag with the given name. This method attempts to read from "data/<name.domain>/tags/<tagType>/<name.path>.json" if the given name is not already present in cache + * @param name + * @param tagType the type of the tag to load, used to resolve paths (blocks, items, entity_types, fluids, functions are the vanilla variants) + * @return + * @throws FileNotFoundException if the file does not exist + */ + public Tag load(NamespaceID name, String tagType) throws FileNotFoundException { + return load(name, tagType, () -> new FileReader(new File(ResourceGatherer.DATA_FOLDER, "data/"+name.getDomain()+"/tags/"+tagType+"/"+name.getPath()+".json"))); + } + + /** + * Loads a tag with the given name. This method attempts to read from 'reader' if the given name is not already present in cache + * @param name + * @param tagType the type of the tag to load, used to resolve paths (blocks, items, entity_types, fluids, functions are the vanilla variants) + * @param reader + * @return + */ + public Tag load(NamespaceID name, String tagType, Reader reader) throws FileNotFoundException { + return load(name, tagType, () -> reader); + } + + /** + * Loads a tag with the given name. This method reads from 'reader'. This will override the previous tag + * @param name + * @param tagType the type of the tag to load, used to resolve paths (blocks, items, entity_types, fluids, functions are the vanilla variants) + * @param readerSupplier + * @return + */ + public Tag forceLoad(NamespaceID name, String tagType, ReaderSupplierWithFileNotFound readerSupplier) throws FileNotFoundException { + Tag prev = cache.getOrDefault(name, Tag.EMPTY); + FileNotFoundException[] ex = new FileNotFoundException[1]; // very ugly code but Java does not let its standard interfaces throw exceptions + Tag result = create(prev, tagType, readerSupplier); + cache.put(name, result); + return result; + } + + /** + * Loads a tag with the given name. This method attempts to read from 'reader' if the given name is not already present in cache + * @param name + * @param tagType the type of the tag to load, used to resolve paths (blocks, items, entity_types, fluids, functions are the vanilla variants) + * @param readerSupplier + * @return + */ + public Tag load(NamespaceID name, String tagType, ReaderSupplierWithFileNotFound readerSupplier) throws FileNotFoundException { + Tag prev = cache.getOrDefault(name, Tag.EMPTY); + FileNotFoundException[] ex = new FileNotFoundException[1]; // very ugly code but Java does not let its standard interfaces throw exceptions + Tag result = cache.computeIfAbsent(name, _name -> { + try { + return create(prev, tagType, readerSupplier); + } catch (FileNotFoundException e) { + ex[0] = e; + return Tag.EMPTY; + } + }); + if(ex[0] != null) { + throw ex[0]; + } + return result; + } + + private Tag create(Tag prev, String tagType, ReaderSupplierWithFileNotFound reader) throws FileNotFoundException { + TagContainer container = gson.fromJson(reader.get(), TagContainer.class); + try { + return new Tag(this, tagType, prev, container); + } catch (FileNotFoundException e) { + LOGGER.error("Failed to load tag due to error", e); + return Tag.EMPTY; + } + } + + public interface ReaderSupplierWithFileNotFound { + Reader get() throws FileNotFoundException; + } +} diff --git a/src/test/java/tags/TestTags.java b/src/test/java/tags/TestTags.java new file mode 100644 index 000000000..3f59ba112 --- /dev/null +++ b/src/test/java/tags/TestTags.java @@ -0,0 +1,104 @@ +package tags; + +import net.minestom.server.gamedata.tags.Tag; +import net.minestom.server.gamedata.tags.TagManager; +import net.minestom.server.utils.NamespaceID; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.FileNotFoundException; +import java.io.StringReader; + +public class TestTags { + + private TagManager tags; + + @Before + public void init() { + tags = new TagManager(); + } + + @Test + public void testSubTag() throws FileNotFoundException { + String tag1 = "{\n" + + "\t\"replace\": false,\n" + + "\t\"values\": [\n" + + "\t\t\"minestom:an_item\"\n" + + "\t]\n" + + "}"; + + String tag2 = "{\n" + + "\t\"replace\": false,\n" + + "\t\"values\": [\n" + + "\t\t\"#minestom:test_sub\",\n" + + "\t\t\"minestom:some_other_item\"\n" + + "\t]\n" + + "}"; + Assert.assertNotEquals(Tag.EMPTY, tags.load(NamespaceID.from("minestom:test_sub"), "any", new StringReader(tag1))); + Tag loaded = tags.load(NamespaceID.from("minestom:test"), "any", new StringReader(tag2)); + NamespaceID[] values = loaded.getValues().toArray(new NamespaceID[0]); + Assert.assertEquals(2, values.length); + Assert.assertTrue(loaded.contains(NamespaceID.from("minestom:an_item"))); + Assert.assertTrue(loaded.contains(NamespaceID.from("minestom:some_other_item"))); + Assert.assertFalse(loaded.contains(NamespaceID.from("minestom:some_other_item_that_is_not_in_the_tag"))); + } + + /** + * A value of 'true' in 'replace' should replace previous contents + */ + @Test + public void testReplacement() throws FileNotFoundException { + String tag1 = "{\n" + + "\t\"replace\": false,\n" + + "\t\"values\": [\n" + + "\t\t\"minestom:an_item\"\n" + + "\t]\n" + + "}"; + + String tag2 = "{\n" + + "\t\"replace\": true,\n" + + "\t\"values\": [\n" + + "\t\t\"minestom:some_other_item\"\n" + + "\t]\n" + + "}"; + Assert.assertNotEquals(Tag.EMPTY, tags.load(NamespaceID.from("minestom:test"), "any", new StringReader(tag1))); + Tag loaded = tags.forceLoad(NamespaceID.from("minestom:test"), "any", () -> new StringReader(tag2)); + Assert.assertNotEquals(Tag.EMPTY, loaded); + Assert.assertEquals(1, loaded.getValues().size()); + Assert.assertTrue(loaded.contains(NamespaceID.from("minestom:some_other_item"))); + Assert.assertFalse(loaded.contains(NamespaceID.from("minestom:an_item"))); + } + + /** + * A value of 'false' in 'replace' should append to previous contents + */ + @Test + public void testAppend() throws FileNotFoundException { + String tag1 = "{\n" + + "\t\"replace\": false,\n" + + "\t\"values\": [\n" + + "\t\t\"minestom:an_item\"\n" + + "\t]\n" + + "}"; + + String tag2 = "{\n" + + "\t\"replace\": false,\n" + + "\t\"values\": [\n" + + "\t\t\"minestom:some_other_item\"\n" + + "\t]\n" + + "}"; + Assert.assertNotEquals(Tag.EMPTY, tags.load(NamespaceID.from("minestom:test"), "any", new StringReader(tag1))); + Tag loaded = tags.forceLoad(NamespaceID.from("minestom:test"), "any", () -> new StringReader(tag2)); + Assert.assertNotEquals(Tag.EMPTY, loaded); + Assert.assertEquals(2, loaded.getValues().size()); + Assert.assertTrue(loaded.contains(NamespaceID.from("minestom:some_other_item"))); + Assert.assertTrue(loaded.contains(NamespaceID.from("minestom:an_item"))); + } + + @After + public void cleanup() { + tags = null; + } +}