mirror of
https://github.com/Minestom/Minestom.git
synced 2024-09-24 20:42:39 +02:00
Loot table support
This commit is contained in:
parent
c50030cd6b
commit
3c0d351f15
@ -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;
|
||||
|
@ -6,6 +6,26 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class Data {
|
||||
|
||||
public static final Data EMPTY = new Data() {
|
||||
@Override
|
||||
public <T> void set(String key, T value, Class<T> type) {}
|
||||
|
||||
@Override
|
||||
public <T> T get(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(String key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getOrDefault(String key, T defaultValue) {
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
protected ConcurrentHashMap<String, Object> data = new ConcurrentHashMap();
|
||||
|
||||
public <T> void set(String key, T value, Class<T> type) {
|
||||
|
17
src/main/java/net/minestom/server/gamedata/Condition.java
Normal file
17
src/main/java/net/minestom/server/gamedata/Condition.java
Normal file
@ -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);
|
||||
}
|
@ -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.<Double>get("explosionPower");
|
||||
}
|
||||
}
|
@ -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<Condition> conditions;
|
||||
|
||||
public ConditionedFunctionWrapper(LootTableFunction baseFunction, Collection<Condition> 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);
|
||||
}
|
||||
}
|
@ -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<LootTable.Pool> pools;
|
||||
|
||||
public LootTable(LootTableType type, List<Pool> pools) {
|
||||
this.type = type;
|
||||
this.pools = pools;
|
||||
}
|
||||
|
||||
public LootTableType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public List<Pool> getPools() {
|
||||
return pools;
|
||||
}
|
||||
|
||||
public List<ItemStack> generate(Data arguments) {
|
||||
if(arguments == null)
|
||||
arguments = Data.EMPTY;
|
||||
List<ItemStack> 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<LootTable.Entry> entries;
|
||||
private final List<Condition> conditions;
|
||||
|
||||
public Pool(int minRollCount, int maxRollCount, int bonusMinRollCount, int bonusMaxRollCount, List<Entry> entries, List<Condition> conditions) {
|
||||
this.minRollCount = minRollCount;
|
||||
this.maxRollCount = maxRollCount;
|
||||
this.bonusMinRollCount = bonusMinRollCount;
|
||||
this.bonusMaxRollCount = bonusMaxRollCount;
|
||||
this.entries = entries;
|
||||
this.conditions = conditions;
|
||||
}
|
||||
|
||||
public List<Condition> getConditions() {
|
||||
return conditions;
|
||||
}
|
||||
|
||||
public int getMinRollCount() {
|
||||
return minRollCount;
|
||||
}
|
||||
|
||||
public int getMaxRollCount() {
|
||||
return maxRollCount;
|
||||
}
|
||||
|
||||
public List<Entry> getEntries() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
public void generate(List<ItemStack> 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<Entry> 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);
|
||||
}
|
||||
}
|
@ -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<LootTable.Pool> 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<LootTable.Entry> entries = new LinkedList<>();
|
||||
List<Condition> 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<Condition> conditions = new LinkedList<>();
|
||||
if(this.conditions != null) {
|
||||
for(ConditionContainer c : this.conditions) {
|
||||
conditions.add(c.create(lootTableManager));
|
||||
}
|
||||
}
|
||||
List<LootTable.Entry> children = new LinkedList<>();
|
||||
if(this.children != null) {
|
||||
for (Entry c : this.children) {
|
||||
children.add(c.create(lootTableManager));
|
||||
}
|
||||
}
|
||||
List<LootTableFunction> 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<Condition> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<Condition> conditions, List<LootTable.Entry> children, boolean expand, List<LootTableFunction> functions, int weight, int quality);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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<Condition> conditions = new NamespaceIDHashMap<>();
|
||||
private NamespaceIDHashMap<LootTableType> tableTypes = new NamespaceIDHashMap<>();
|
||||
private NamespaceIDHashMap<LootTableEntryType> entryTypes = new NamespaceIDHashMap<>();
|
||||
private NamespaceIDHashMap<LootTableFunction> functions = new NamespaceIDHashMap<>();
|
||||
private NamespaceIDHashMap<LootTable> 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);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package net.minestom.server.gamedata.loottables;
|
||||
|
||||
public interface LootTableType {
|
||||
|
||||
// TODO
|
||||
}
|
@ -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<RangeContainer> {
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<LootTableFunction> functions;
|
||||
private final List<Condition> conditions;
|
||||
private final Material item;
|
||||
|
||||
ItemEntry(ItemType type, Material baseItem, int weight, int quality, List<LootTableFunction> functions, List<Condition> 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<LootTableFunction> getFunctions() {
|
||||
return functions;
|
||||
}
|
||||
|
||||
public List<Condition> getConditions() {
|
||||
return conditions;
|
||||
}
|
||||
|
||||
public Material getItem() {
|
||||
return item;
|
||||
}
|
||||
}
|
@ -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<Condition> conditions, List<LootTable.Entry> children, boolean expand, List<LootTableFunction> 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);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package net.minestom.server.gamedata.loottables.tabletypes;
|
||||
|
||||
import net.minestom.server.gamedata.loottables.LootTableType;
|
||||
|
||||
public class BlockType implements LootTableType {
|
||||
}
|
119
src/main/java/net/minestom/server/utils/NamespaceID.java
Normal file
119
src/main/java/net/minestom/server/utils/NamespaceID.java
Normal file
@ -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<NamespaceID> 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 <pre>namespaceID</pre> 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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package net.minestom.server.utils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class NamespaceIDHashMap<V> extends AbstractMap<NamespaceID, V> {
|
||||
|
||||
private final Map<NamespaceID, V> backing = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Set<Entry<NamespaceID, V>> 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<? super NamespaceID, ? extends V> 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<? super NamespaceID, ? extends V> 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);
|
||||
}
|
||||
}
|
46
src/main/java/net/minestom/server/utils/WeightedRandom.java
Normal file
46
src/main/java/net/minestom/server/utils/WeightedRandom.java
Normal file
@ -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 <E>
|
||||
*/
|
||||
public class WeightedRandom<E extends WeightedRandomItem> {
|
||||
|
||||
private final List<E> entries;
|
||||
private final List<Double> weightSums;
|
||||
private final double totalWeight;
|
||||
|
||||
public WeightedRandom(Collection<E> 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);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package net.minestom.server.utils;
|
||||
|
||||
public interface WeightedRandomItem {
|
||||
|
||||
double getWeight();
|
||||
|
||||
}
|
135
src/test/java/loottables/TestLootTables.java
Normal file
135
src/test/java/loottables/TestLootTables.java
Normal file
@ -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<ItemStack> 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<ItemStack> stacks = lootTable.generate(arguments);
|
||||
Assert.assertEquals(0, stacks.size());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user