diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index 2e7a9a81d..61db0d05e 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -5,6 +5,7 @@ import net.minestom.server.command.CommandManager; 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.instance.InstanceManager; import net.minestom.server.instance.block.BlockManager; import net.minestom.server.listener.manager.PacketListenerManager; @@ -90,6 +91,7 @@ public class MinecraftServer { // Data private static ResponseDataConsumer responseDataConsumer; private static Difficulty difficulty = Difficulty.NORMAL; + private static LootTableManager lootTableManager; public static MinecraftServer init() { connectionManager = new ConnectionManager(); @@ -109,6 +111,8 @@ public class MinecraftServer { updateManager = new UpdateManager(); + lootTableManager = new LootTableManager(); + nettyServer = new NettyServer(packetProcessor); // Registry @@ -201,6 +205,10 @@ public class MinecraftServer { return responseDataConsumer; } + public static LootTableManager getLootTableManager() { + return lootTableManager; + } + 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/data/Data.java b/src/main/java/net/minestom/server/data/Data.java index 3ce858195..8ac12c96f 100644 --- a/src/main/java/net/minestom/server/data/Data.java +++ b/src/main/java/net/minestom/server/data/Data.java @@ -6,6 +6,26 @@ import java.util.concurrent.ConcurrentHashMap; public class Data { + public static final Data EMPTY = new Data() { + @Override + public void set(String key, T value, Class type) {} + + @Override + public T get(String key) { + return null; + } + + @Override + public boolean hasKey(String key) { + return false; + } + + @Override + public T getOrDefault(String key, T defaultValue) { + return defaultValue; + } + }; + protected ConcurrentHashMap data = new ConcurrentHashMap(); public void set(String key, T value, Class type) { diff --git a/src/main/java/net/minestom/server/gamedata/Condition.java b/src/main/java/net/minestom/server/gamedata/Condition.java new file mode 100644 index 000000000..1bd691e44 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/Condition.java @@ -0,0 +1,17 @@ +package net.minestom.server.gamedata; + +import net.minestom.server.data.Data; + +/** + * Represents a condition, used by predicates in MC functions and in loot tables. + */ +@FunctionalInterface +public interface Condition { + + /** + * Tests this condition. Subclasses are free to throw runtime exceptions if the arguments passed through data are not valid or missing + * @param data arguments to give to the condition. May be null if the condition supports it + * @return 'true' if the condition passed, 'false' otherwise + */ + boolean test(Data data); +} diff --git a/src/main/java/net/minestom/server/gamedata/conditions/SurvivesExplosionCondition.java b/src/main/java/net/minestom/server/gamedata/conditions/SurvivesExplosionCondition.java new file mode 100644 index 000000000..2dc0080b0 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/conditions/SurvivesExplosionCondition.java @@ -0,0 +1,22 @@ +package net.minestom.server.gamedata.conditions; + +import net.minestom.server.data.Data; +import net.minestom.server.gamedata.Condition; + +import java.util.Random; + +/** + * Requires 'explosionPower' double argument + */ +public class SurvivesExplosionCondition implements Condition { + private Random rng = new Random(); + + @Override + public boolean test(Data data) { + if(data == null) + return true; // no explosion here + if(!data.hasKey("explosionPower")) + return true; // no explosion here + return rng.nextDouble() <= 1.0/data.get("explosionPower"); + } +} diff --git a/src/main/java/net/minestom/server/gamedata/loottables/ConditionedFunctionWrapper.java b/src/main/java/net/minestom/server/gamedata/loottables/ConditionedFunctionWrapper.java new file mode 100644 index 000000000..93c6f0c00 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/loottables/ConditionedFunctionWrapper.java @@ -0,0 +1,30 @@ +package net.minestom.server.gamedata.loottables; + +import net.minestom.server.data.Data; +import net.minestom.server.gamedata.Condition; +import net.minestom.server.item.ItemStack; + +import java.util.Collection; + +/** + * Loot table function that must meet some conditions to be applied + */ +public class ConditionedFunctionWrapper implements LootTableFunction { + + private final LootTableFunction baseFunction; + private final Collection conditions; + + public ConditionedFunctionWrapper(LootTableFunction baseFunction, Collection conditions) { + this.baseFunction = baseFunction; + this.conditions = conditions; + } + + @Override + public ItemStack apply(ItemStack stack, Data data) { + for (Condition c : conditions) { + if(!c.test(data)) + return stack; + } + return baseFunction.apply(stack, data); + } +} diff --git a/src/main/java/net/minestom/server/gamedata/loottables/LootTable.java b/src/main/java/net/minestom/server/gamedata/loottables/LootTable.java new file mode 100644 index 000000000..def0e7992 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/loottables/LootTable.java @@ -0,0 +1,122 @@ +package net.minestom.server.gamedata.loottables; + +import net.minestom.server.data.Data; +import net.minestom.server.gamedata.Condition; +import net.minestom.server.item.ItemStack; +import net.minestom.server.utils.WeightedRandom; +import net.minestom.server.utils.WeightedRandomItem; + +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + +public class LootTable { + + public static final String LUCK_KEY = "minecraft:luck"; + + private final LootTableType type; + private final List pools; + + public LootTable(LootTableType type, List pools) { + this.type = type; + this.pools = pools; + } + + public LootTableType getType() { + return type; + } + + public List getPools() { + return pools; + } + + public List generate(Data arguments) { + if(arguments == null) + arguments = Data.EMPTY; + List output = new LinkedList<>(); + for(Pool p : pools) { + p.generate(output, arguments); + } + return output; + } + + public static class Pool { + private final int minRollCount; + private final int maxRollCount; + private final int bonusMinRollCount; + private final int bonusMaxRollCount; + private final List entries; + private final List conditions; + + public Pool(int minRollCount, int maxRollCount, int bonusMinRollCount, int bonusMaxRollCount, List entries, List conditions) { + this.minRollCount = minRollCount; + this.maxRollCount = maxRollCount; + this.bonusMinRollCount = bonusMinRollCount; + this.bonusMaxRollCount = bonusMaxRollCount; + this.entries = entries; + this.conditions = conditions; + } + + public List getConditions() { + return conditions; + } + + public int getMinRollCount() { + return minRollCount; + } + + public int getMaxRollCount() { + return maxRollCount; + } + + public List getEntries() { + return entries; + } + + public void generate(List output, Data arguments) { + for(Condition c : conditions) { + if(!c.test(arguments)) + return; + } + Random rng = new Random(); + int luck = arguments.getOrDefault(LUCK_KEY, 0); + int rollCount = rng.nextInt(maxRollCount - minRollCount +1 /*inclusive*/) + minRollCount; + int bonusRollCount = rng.nextInt(bonusMaxRollCount - bonusMinRollCount +1 /*inclusive*/) + bonusMinRollCount; + bonusRollCount *= luck; + WeightedRandom weightedRandom = new WeightedRandom<>(entries); + for (int i = 0; i < rollCount+bonusRollCount; i++) { + Entry entry = weightedRandom.get(rng); + ItemStack stack = entry.generateStack(arguments); + if(!stack.isAir()) { + output.add(stack); + } + } + } + } + + public abstract static class Entry implements WeightedRandomItem { + private final LootTableEntryType type; + private final int weight; + private final int quality; + + public Entry(LootTableEntryType type, int weight, int quality) { + this.type = type; + this.weight = weight; + this.quality = quality; + } + + public int getQuality() { + return quality; + } + + public double getWeight() { + return weight; + } + + public LootTableEntryType getType() { + return type; + } + + public abstract ItemStack generateStack(Data arguments); + } +} diff --git a/src/main/java/net/minestom/server/gamedata/loottables/LootTableContainer.java b/src/main/java/net/minestom/server/gamedata/loottables/LootTableContainer.java new file mode 100644 index 000000000..2149a1a78 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/loottables/LootTableContainer.java @@ -0,0 +1,125 @@ +package net.minestom.server.gamedata.loottables; + +import net.minestom.server.gamedata.Condition; +import net.minestom.server.utils.NamespaceID; + +import java.util.LinkedList; +import java.util.List; + +/** + * Meant only for parsing loot tables + */ +class LootTableContainer { + + + private String type; + private LootTableContainer.Pool[] pools; + + private LootTableContainer() {} + + public LootTable createTable(LootTableManager lootTableManager) { + LootTableType type = lootTableManager.getTableType(NamespaceID.from(this.type)); + List pools = new LinkedList<>(); + if(this.pools != null) { + for(Pool p : this.pools) { + pools.add(p.create(lootTableManager)); + } + } + return new LootTable(type, pools); + } + + private class Pool { + private ConditionContainer[] conditions; + private FunctionContainer[] functions; + private RangeContainer rolls; + private RangeContainer bonus_rools; + + private Entry[] entries; + + private Pool() {} + + public LootTable.Pool create(LootTableManager lootTableManager) { + List entries = new LinkedList<>(); + List conditions = new LinkedList<>(); + if(this.entries != null) { + for (Entry e : this.entries) { + entries.add(e.create(lootTableManager)); + } + } + if(this.conditions != null) { + for (ConditionContainer c : this.conditions) { + conditions.add(c.create(lootTableManager)); + } + } + if(rolls == null) + rolls = new RangeContainer(0,0); + if(bonus_rools == null) + bonus_rools = new RangeContainer(0,0); + return new LootTable.Pool(rolls.getMin(), rolls.getMax(), bonus_rools.getMin(), bonus_rools.getMax(), entries, conditions); + } + } + + private class Entry { + private ConditionContainer[] conditions; + private String type; + private String name; + private Entry[] children; + private boolean expand; + private FunctionContainer[] functions; + private int weight; + private int quality; + + private Entry() {} + + public LootTable.Entry create(LootTableManager lootTableManager) { + LootTableEntryType entryType = lootTableManager.getEntryType(NamespaceID.from(type)); + List conditions = new LinkedList<>(); + if(this.conditions != null) { + for(ConditionContainer c : this.conditions) { + conditions.add(c.create(lootTableManager)); + } + } + List children = new LinkedList<>(); + if(this.children != null) { + for (Entry c : this.children) { + children.add(c.create(lootTableManager)); + } + } + List functions = new LinkedList<>(); + if(this.functions != null) { + for(FunctionContainer c : this.functions) { + functions.add(c.create(lootTableManager)); + } + } + return entryType.create(lootTableManager, name, conditions, children, expand, functions, weight, quality); + } + } + + private class ConditionContainer { + private String condition; + + private ConditionContainer() {} + + public Condition create(LootTableManager lootTableManager) { + return lootTableManager.getCondition(NamespaceID.from(condition)); + } + } + + private class FunctionContainer { + private String function; + private ConditionContainer[] conditions; + + private FunctionContainer() {} + + public LootTableFunction create(LootTableManager lootTableManager) { + List conditions = new LinkedList<>(); + if(this.conditions != null) { + for(ConditionContainer c : this.conditions) { + conditions.add(c.create(lootTableManager)); + } + } + return new ConditionedFunctionWrapper(lootTableManager.getFunction(NamespaceID.from(function)), conditions); + } + } + +} diff --git a/src/main/java/net/minestom/server/gamedata/loottables/LootTableEntryType.java b/src/main/java/net/minestom/server/gamedata/loottables/LootTableEntryType.java new file mode 100644 index 000000000..fa1a817d3 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/loottables/LootTableEntryType.java @@ -0,0 +1,10 @@ +package net.minestom.server.gamedata.loottables; + +import net.minestom.server.gamedata.Condition; + +import java.util.List; + +@FunctionalInterface +public interface LootTableEntryType { + LootTable.Entry create(LootTableManager lootTableManager, String name, List conditions, List children, boolean expand, List functions, int weight, int quality); +} diff --git a/src/main/java/net/minestom/server/gamedata/loottables/LootTableFunction.java b/src/main/java/net/minestom/server/gamedata/loottables/LootTableFunction.java new file mode 100644 index 000000000..a430034b7 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/loottables/LootTableFunction.java @@ -0,0 +1,20 @@ +package net.minestom.server.gamedata.loottables; + +import net.minestom.server.data.Data; +import net.minestom.server.item.ItemStack; + +/** + * Changes to apply to the stack being produced + */ +@FunctionalInterface +public interface LootTableFunction { + + /** + * Applies changes to the stack being produced + * @param stack + * @param data arguments to pass to the function. + * @return + */ + ItemStack apply(ItemStack stack, Data data); +} + diff --git a/src/main/java/net/minestom/server/gamedata/loottables/LootTableManager.java b/src/main/java/net/minestom/server/gamedata/loottables/LootTableManager.java new file mode 100644 index 000000000..8f36115df --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/loottables/LootTableManager.java @@ -0,0 +1,106 @@ +package net.minestom.server.gamedata.loottables; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.minestom.server.gamedata.Condition; +import net.minestom.server.registry.ResourceGatherer; +import net.minestom.server.utils.NamespaceID; +import net.minestom.server.utils.NamespaceIDHashMap; + +import java.io.*; + +/** + * Handles loading and configuration of loot tables + */ +public class LootTableManager { + + private NamespaceIDHashMap conditions = new NamespaceIDHashMap<>(); + private NamespaceIDHashMap tableTypes = new NamespaceIDHashMap<>(); + private NamespaceIDHashMap entryTypes = new NamespaceIDHashMap<>(); + private NamespaceIDHashMap functions = new NamespaceIDHashMap<>(); + private NamespaceIDHashMap cache = new NamespaceIDHashMap<>(); + private static Gson gson; + + static { + gson = new GsonBuilder() + .registerTypeAdapter(RangeContainer.class, new RangeContainer.Deserializer()) + .create(); + } + + /** + * Registers a condition to the given namespaceID + * @param namespaceID + * @param condition + */ + public void registerCondition(NamespaceID namespaceID, Condition condition) { + conditions.put(namespaceID, condition); + } + + /** + * Registers a loot table type to the given namespaceID + * @param namespaceID + * @param type + */ + public void registerTableType(NamespaceID namespaceID, LootTableType type) { + tableTypes.put(namespaceID, type); + } + + /** + * Registers a loot table entry type to the given namespaceID + * @param namespaceID + * @param type + */ + public void registerEntryType(NamespaceID namespaceID, LootTableEntryType type) { + entryTypes.put(namespaceID, type); + } + + /** + * Registers a loot table function to the given namespaceID + * @param namespaceID + * @param function + */ + public void registerFunction(NamespaceID namespaceID, LootTableFunction function) { + functions.put(namespaceID, function); + } + + public LootTable load(NamespaceID name) throws FileNotFoundException { + return load(name, new FileReader(new File(ResourceGatherer.DATA_FOLDER, "data/"+name.getDomain()+"/loot_tables/"+name.getPath()+".json"))); + } + + /** + * Loads a loot table with the given name. Loot tables can be cached, so 'reader' is used only on cache misses + * @param name the name to cache the loot table with + * @param reader the reader to read the loot table from, if none cached. **Will** be closed no matter the results of this call + * @return + */ + public LootTable load(NamespaceID name, Reader reader) { + try { + return cache.computeIfAbsent(name, _name -> create(reader)); + } finally { + try { + reader.close(); + } catch (IOException e) {} + } + } + + private LootTable create(Reader reader) { + LootTableContainer container = gson.fromJson(reader, LootTableContainer.class); + return container.createTable(this); + } + + public Condition getCondition(NamespaceID id) { + return conditions.get(id); + } + + public LootTableType getTableType(NamespaceID id) { + return tableTypes.get(id); + } + + public LootTableEntryType getEntryType(NamespaceID id) { + return entryTypes.get(id); + } + + public LootTableFunction getFunction(NamespaceID id) { + return functions.get(id); + } +} diff --git a/src/main/java/net/minestom/server/gamedata/loottables/LootTableType.java b/src/main/java/net/minestom/server/gamedata/loottables/LootTableType.java new file mode 100644 index 000000000..053e29806 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/loottables/LootTableType.java @@ -0,0 +1,6 @@ +package net.minestom.server.gamedata.loottables; + +public interface LootTableType { + + // TODO +} diff --git a/src/main/java/net/minestom/server/gamedata/loottables/RangeContainer.java b/src/main/java/net/minestom/server/gamedata/loottables/RangeContainer.java new file mode 100644 index 000000000..7a415a1b8 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/loottables/RangeContainer.java @@ -0,0 +1,49 @@ +package net.minestom.server.gamedata.loottables; + +import com.google.gson.*; + +import java.lang.reflect.Type; + +public class RangeContainer { + + private int min; + private int max; + + RangeContainer() {} + + public RangeContainer(int min, int max) { + this.min = min; + this.max = max; + } + + public int getMin() { + return min; + } + + public int getMax() { + return max; + } + + public static class Deserializer implements JsonDeserializer { + + @Override + public RangeContainer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + RangeContainer range = new RangeContainer(); + if(json.isJsonPrimitive()) { + range.min = json.getAsInt(); + range.max = json.getAsInt(); + } else if(json.isJsonObject()) { + JsonObject obj = json.getAsJsonObject(); + if(!obj.has("min")) + throw new IllegalArgumentException("Missing 'min' property"); + if(!obj.has("max")) + throw new IllegalArgumentException("Missing 'max' property"); + range.min = obj.get("min").getAsInt(); + range.max = obj.get("max").getAsInt(); + } else { + throw new IllegalArgumentException("Range must be single integer or an object with 'min' and 'max' properties"); + } + return range; + } + } +} diff --git a/src/main/java/net/minestom/server/gamedata/loottables/entries/ItemEntry.java b/src/main/java/net/minestom/server/gamedata/loottables/entries/ItemEntry.java new file mode 100644 index 000000000..731c7a098 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/loottables/entries/ItemEntry.java @@ -0,0 +1,50 @@ +package net.minestom.server.gamedata.loottables.entries; + +import net.minestom.server.data.Data; +import net.minestom.server.gamedata.Condition; +import net.minestom.server.gamedata.loottables.LootTable; +import net.minestom.server.gamedata.loottables.LootTableFunction; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; + +import java.util.LinkedList; +import java.util.List; + +public class ItemEntry extends LootTable.Entry { + + private final List functions; + private final List conditions; + private final Material item; + + ItemEntry(ItemType type, Material baseItem, int weight, int quality, List functions, List conditions) { + super(type, weight, quality); + this.item = baseItem; + this.functions = new LinkedList<>(functions); + this.conditions = new LinkedList<>(conditions); + } + + @Override + public ItemStack generateStack(Data arguments) { + for(Condition c : conditions) { + if(!c.test(arguments)) + return ItemStack.getAirItem(); + } + ItemStack stack = new ItemStack(item, (byte)1); + for (LootTableFunction function : functions) { + stack = function.apply(stack, arguments); + } + return stack; + } + + public List getFunctions() { + return functions; + } + + public List getConditions() { + return conditions; + } + + public Material getItem() { + return item; + } +} diff --git a/src/main/java/net/minestom/server/gamedata/loottables/entries/ItemType.java b/src/main/java/net/minestom/server/gamedata/loottables/entries/ItemType.java new file mode 100644 index 000000000..cf73c55ad --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/loottables/entries/ItemType.java @@ -0,0 +1,20 @@ +package net.minestom.server.gamedata.loottables.entries; + +import net.minestom.server.gamedata.Condition; +import net.minestom.server.gamedata.loottables.LootTable; +import net.minestom.server.gamedata.loottables.LootTableEntryType; +import net.minestom.server.gamedata.loottables.LootTableFunction; +import net.minestom.server.gamedata.loottables.LootTableManager; +import net.minestom.server.item.Material; +import net.minestom.server.utils.NamespaceID; + +import java.util.List; + +public class ItemType implements LootTableEntryType { + @Override + public LootTable.Entry create(LootTableManager lootTableManager, String name, List conditions, List children, boolean expand, List functions, int weight, int quality) { + NamespaceID itemID = NamespaceID.from(name); + // TODO: handle non-vanilla IDs ? + return new ItemEntry(this, Material.valueOf(itemID.getPath().toUpperCase()), weight, quality, functions, conditions); + } +} diff --git a/src/main/java/net/minestom/server/gamedata/loottables/tabletypes/BlockType.java b/src/main/java/net/minestom/server/gamedata/loottables/tabletypes/BlockType.java new file mode 100644 index 000000000..e41500785 --- /dev/null +++ b/src/main/java/net/minestom/server/gamedata/loottables/tabletypes/BlockType.java @@ -0,0 +1,6 @@ +package net.minestom.server.gamedata.loottables.tabletypes; + +import net.minestom.server.gamedata.loottables.LootTableType; + +public class BlockType implements LootTableType { +} diff --git a/src/main/java/net/minestom/server/utils/NamespaceID.java b/src/main/java/net/minestom/server/utils/NamespaceID.java new file mode 100644 index 000000000..fd0ead4ce --- /dev/null +++ b/src/main/java/net/minestom/server/utils/NamespaceID.java @@ -0,0 +1,119 @@ +package net.minestom.server.utils; + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import java.util.Objects; + +/** + * Represents a namespaced ID + * https://minecraft.gamepedia.com/Namespaced_ID + * + * TODO: Implement validity conditions + */ +public class NamespaceID implements CharSequence { + private static final Int2ObjectOpenHashMap cache = new Int2ObjectOpenHashMap<>(); + + private final String domain; + private final String path; + private final String full; + + /** + * Extracts the domain from the namespace ID. "minecraft:stone" would return "minecraft". + * If no ':' character is found, "minecraft" is returned. + * @param namespaceID + * @return the domain of the namespace ID + */ + public static String getDomain(String namespaceID) { + int index = namespaceID.indexOf(':'); + if(index < 0) + return "minecraft"; + return namespaceID.substring(0, index); + } + + /** + * Extracts the path from the namespace ID. "minecraft:blocks/stone" would return "blocks/stone". + * If no ':' character is found, the
namespaceID
is returned. + * @param namespaceID + * @return the path of the namespace ID + */ + public static String getPath(String namespaceID) { + int index = namespaceID.indexOf(':'); + if(index < 0) + return namespaceID; + return namespaceID.substring(index+1); + } + + static int hash(String domain, String path) { + return Objects.hash(domain, path); + } + + public static NamespaceID from(String domain, String path) { + int hash = hash(domain, path); + return cache.computeIfAbsent(hash, _unused -> new NamespaceID(domain, path)); + } + + public static NamespaceID from(String id) { + return from(getDomain(id), getPath(id)); + } + + private NamespaceID(String path) { + int index = path.indexOf(':'); + if(index < 0) { + this.domain = "minecraft"; + this.path = path; + } else { + this.domain = path.substring(0, index); + this.path = path.substring(index+1); + } + this.full = toString(); + } + + private NamespaceID(String domain, String path) { + this.domain = domain; + this.path = path; + this.full = toString(); + } + + public String getDomain() { + return domain; + } + + public String getPath() { + return path; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NamespaceID that = (NamespaceID) o; + return Objects.equals(domain, that.domain) && + Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return hash(domain, path); + } + + @Override + public int length() { + return full.length(); + } + + @Override + public char charAt(int index) { + return full.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return full.subSequence(start, end); + } + + @Override + public String toString() { + return domain+":"+path; + } + +} diff --git a/src/main/java/net/minestom/server/utils/NamespaceIDHashMap.java b/src/main/java/net/minestom/server/utils/NamespaceIDHashMap.java new file mode 100644 index 000000000..4e1e83fb4 --- /dev/null +++ b/src/main/java/net/minestom/server/utils/NamespaceIDHashMap.java @@ -0,0 +1,64 @@ +package net.minestom.server.utils; + +import java.util.*; +import java.util.function.Function; + +public class NamespaceIDHashMap extends AbstractMap { + + private final Map backing = new HashMap<>(); + + @Override + public Set> entrySet() { + return backing.entrySet(); + } + + @Override + public V get(Object key) { + return backing.get(key); + } + + @Override + public V put(NamespaceID key, V value) { + return backing.put(key, value); + } + + public boolean containsKey(String id) { + return containsKey(NamespaceID.getDomain(id), NamespaceID.getPath(id)); + } + + public boolean containsKey(String domain, String path) { + return backing.containsKey(NamespaceID.from(domain, path)); + } + + public V get(String id) { + return get(NamespaceID.getDomain(id), NamespaceID.getPath(id)); + } + + public V get(String domain, String path) { + return backing.get(NamespaceID.from(domain, path)); + } + + public V put(String domain, String path, V value) { + return put(NamespaceID.from(domain, path), value); + } + + public V computeIfAbsent(String domain, String path, Function mappingFunction) { + return computeIfAbsent(NamespaceID.from(domain, path), mappingFunction); + } + + public V put(String id, V value) { + return put(NamespaceID.from(id), value); + } + + public V computeIfAbsent(String id, Function mappingFunction) { + return computeIfAbsent(NamespaceID.from(id), mappingFunction); + } + + public V getOrDefault(String id, V defaultValue) { + return getOrDefault(NamespaceID.from(id), defaultValue); + } + + public V getOrDefault(String domain, String path, V defaultValue) { + return getOrDefault(NamespaceID.from(domain, path), defaultValue); + } +} diff --git a/src/main/java/net/minestom/server/utils/WeightedRandom.java b/src/main/java/net/minestom/server/utils/WeightedRandom.java new file mode 100644 index 000000000..4d09e5041 --- /dev/null +++ b/src/main/java/net/minestom/server/utils/WeightedRandom.java @@ -0,0 +1,46 @@ +package net.minestom.server.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; + +/** + * Produces a random element from a given set, with weights applied + * @param + */ +public class WeightedRandom { + + private final List entries; + private final List weightSums; + private final double totalWeight; + + public WeightedRandom(Collection items) { + if(items.isEmpty()) + throw new IllegalArgumentException("items must not be empty"); + this.entries = new ArrayList<>(items); + this.weightSums = new ArrayList<>(items.size()); + double sum = 0.0; + for(E item : items) { + sum += item.getWeight(); + weightSums.add(sum); + } + this.totalWeight = sum; + } + + /** + * Gets a random element from this set + * @param rng Random Number Generator to generate random numbers with + * @return + */ + public E get(Random rng) { + double p = rng.nextDouble()*totalWeight; + for (int i = 0; i < entries.size(); i++) { + double weightSum = weightSums.get(i); + if(weightSum >= p) { + return entries.get(i); + } + } + return entries.get(entries.size()-1); + } +} diff --git a/src/main/java/net/minestom/server/utils/WeightedRandomItem.java b/src/main/java/net/minestom/server/utils/WeightedRandomItem.java new file mode 100644 index 000000000..ec4aea423 --- /dev/null +++ b/src/main/java/net/minestom/server/utils/WeightedRandomItem.java @@ -0,0 +1,7 @@ +package net.minestom.server.utils; + +public interface WeightedRandomItem { + + double getWeight(); + +} diff --git a/src/test/java/loottables/TestLootTables.java b/src/test/java/loottables/TestLootTables.java new file mode 100644 index 000000000..91946671b --- /dev/null +++ b/src/test/java/loottables/TestLootTables.java @@ -0,0 +1,135 @@ +package loottables; + +import net.minestom.server.data.Data; +import net.minestom.server.gamedata.conditions.SurvivesExplosionCondition; +import net.minestom.server.gamedata.loottables.LootTable; +import net.minestom.server.gamedata.loottables.LootTableManager; +import net.minestom.server.gamedata.loottables.entries.ItemEntry; +import net.minestom.server.gamedata.loottables.entries.ItemType; +import net.minestom.server.gamedata.loottables.tabletypes.BlockType; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.registry.RegistryMain; +import net.minestom.server.utils.NamespaceID; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.FileNotFoundException; +import java.io.StringReader; +import java.util.List; + +public class TestLootTables { + + private LootTableManager tableManager; + + @Before + public void init() { + RegistryMain.registerBlocks(); + RegistryMain.registerItems(); + tableManager = new LootTableManager(); + tableManager.registerCondition(NamespaceID.from("minecraft:survives_explosion"), new SurvivesExplosionCondition()); + tableManager.registerTableType(NamespaceID.from("minecraft:block"), new BlockType()); + tableManager.registerEntryType(NamespaceID.from("minecraft:item"), new ItemType()); + } + + @Test + public void loadFromString() { + // from acacia_button.json + final String lootTableJson = "{\n" + + " \"type\": \"minecraft:block\",\n" + + " \"pools\": [\n" + + " {\n" + + " \"rolls\": 1,\n" + + " \"entries\": [\n" + + " {\n" + + " \"type\": \"minecraft:item\",\n" + + " \"name\": \"minecraft:acacia_button\"\n" + + " }\n" + + " ],\n" + + " \"conditions\": [\n" + + " {\n" + + " \"condition\": \"minecraft:survives_explosion\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + LootTable lootTable = tableManager.load(NamespaceID.from("blocks/acacia_button"), new StringReader(lootTableJson)); + Assert.assertTrue(lootTable.getType() instanceof BlockType); + Assert.assertEquals(1, lootTable.getPools().size()); + Assert.assertEquals(1, lootTable.getPools().get(0).getMinRollCount()); + Assert.assertEquals(1, lootTable.getPools().get(0).getMaxRollCount()); + Assert.assertEquals(1, lootTable.getPools().get(0).getEntries().size()); + Assert.assertTrue(lootTable.getPools().get(0).getEntries().get(0).getType() instanceof ItemType); + Assert.assertTrue(lootTable.getPools().get(0).getEntries().get(0) instanceof ItemEntry); + ItemEntry entry = (ItemEntry) lootTable.getPools().get(0).getEntries().get(0); + Assert.assertEquals(Material.ACACIA_BUTTON, entry.getItem()); + Assert.assertEquals(0, entry.getFunctions().size()); + Assert.assertEquals(1, lootTable.getPools().get(0).getConditions().size()); + Assert.assertTrue(lootTable.getPools().get(0).getConditions().get(0) instanceof SurvivesExplosionCondition); + } + + @Test + public void loadFromFile() throws FileNotFoundException { + // from acacia_button.json + final String lootTableJson = "{\n" + + " \"type\": \"minecraft:block\",\n" + + " \"pools\": [\n" + + " {\n" + + " \"rolls\": 1,\n" + + " \"entries\": [\n" + + " {\n" + + " \"type\": \"minecraft:item\",\n" + + " \"name\": \"minecraft:acacia_button\"\n" + + " }\n" + + " ],\n" + + " \"conditions\": [\n" + + " {\n" + + " \"condition\": \"minecraft:survives_explosion\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + LootTable lootTable = tableManager.load(NamespaceID.from("blocks/acacia_button")); + Assert.assertTrue(lootTable.getType() instanceof BlockType); + Assert.assertEquals(1, lootTable.getPools().size()); + Assert.assertEquals(1, lootTable.getPools().get(0).getMinRollCount()); + Assert.assertEquals(1, lootTable.getPools().get(0).getMaxRollCount()); + Assert.assertEquals(1, lootTable.getPools().get(0).getEntries().size()); + Assert.assertTrue(lootTable.getPools().get(0).getEntries().get(0).getType() instanceof ItemType); + Assert.assertTrue(lootTable.getPools().get(0).getEntries().get(0) instanceof ItemEntry); + ItemEntry entry = (ItemEntry) lootTable.getPools().get(0).getEntries().get(0); + Assert.assertEquals(Material.ACACIA_BUTTON, entry.getItem()); + Assert.assertEquals(0, entry.getFunctions().size()); + Assert.assertEquals(1, lootTable.getPools().get(0).getConditions().size()); + Assert.assertTrue(lootTable.getPools().get(0).getConditions().get(0) instanceof SurvivesExplosionCondition); + } + + @Test + public void caching() throws FileNotFoundException { + LootTable lootTable1 = tableManager.load(NamespaceID.from("blocks/acacia_button")); + LootTable lootTable2 = tableManager.load(NamespaceID.from("blocks/acacia_button")); + Assert.assertSame(lootTable1, lootTable2); + } + + @Test + public void simpleGenerate() throws FileNotFoundException { + LootTable lootTable = tableManager.load(NamespaceID.from("blocks/acacia_button")); + Data arguments = new Data(); + List stacks = lootTable.generate(arguments); + Assert.assertEquals(1, stacks.size()); + Assert.assertEquals(Material.ACACIA_BUTTON, stacks.get(0).getMaterial()); + } + + @Test + public void testExplosion() throws FileNotFoundException { + LootTable lootTable = tableManager.load(NamespaceID.from("blocks/acacia_button")); + Data arguments = new Data(); + // negative value will force the condition to fail + arguments.set("explosionPower", -1.0, Double.class); + List stacks = lootTable.generate(arguments); + Assert.assertEquals(0, stacks.size()); + } +}