mirror of https://github.com/Minestom/Minestom.git
552 lines
18 KiB
Java
552 lines
18 KiB
Java
package net.minestom.server.registry;
|
|
|
|
import com.google.gson.ToNumberPolicy;
|
|
import com.google.gson.stream.JsonReader;
|
|
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
|
|
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
|
import net.minestom.server.MinecraftServer;
|
|
import net.minestom.server.entity.EntitySpawnType;
|
|
import net.minestom.server.entity.EquipmentSlot;
|
|
import net.minestom.server.instance.block.Block;
|
|
import net.minestom.server.item.Material;
|
|
import net.minestom.server.utils.NamespaceID;
|
|
import net.minestom.server.utils.ObjectArray;
|
|
import net.minestom.server.utils.validate.Check;
|
|
import org.jetbrains.annotations.ApiStatus;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.util.*;
|
|
import java.util.function.Supplier;
|
|
|
|
/**
|
|
* Handles registry data, used by {@link ProtocolObject} implementations and is strictly internal.
|
|
* Use at your own risk.
|
|
*/
|
|
public final class Registry {
|
|
@ApiStatus.Internal
|
|
public static BlockEntry block(String namespace, @NotNull Map<String, Object> jsonObject, Map<String, Object> override) {
|
|
return new BlockEntry(namespace, jsonObject, override);
|
|
}
|
|
|
|
@ApiStatus.Internal
|
|
public static MaterialEntry material(String namespace, @NotNull Map<String, Object> jsonObject, Map<String, Object> override) {
|
|
return new MaterialEntry(namespace, jsonObject, override);
|
|
}
|
|
|
|
@ApiStatus.Internal
|
|
public static EntityEntry entity(String namespace, @NotNull Map<String, Object> jsonObject, Map<String, Object> override) {
|
|
return new EntityEntry(namespace, jsonObject, override);
|
|
}
|
|
|
|
@ApiStatus.Internal
|
|
public static EnchantmentEntry enchantment(String namespace, @NotNull Map<String, Object> jsonObject, Map<String, Object> override) {
|
|
return new EnchantmentEntry(namespace, jsonObject, override);
|
|
}
|
|
|
|
@ApiStatus.Internal
|
|
public static PotionEffectEntry potionEffect(String namespace, @NotNull Map<String, Object> jsonObject, Map<String, Object> override) {
|
|
return new PotionEffectEntry(namespace, jsonObject, override);
|
|
}
|
|
|
|
@ApiStatus.Internal
|
|
public static Map<String, Map<String, Object>> load(Resource resource) {
|
|
Map<String, Map<String, Object>> map = new HashMap<>();
|
|
try (InputStream resourceStream = Registry.class.getClassLoader().getResourceAsStream(resource.name)) {
|
|
Check.notNull(resourceStream, "Resource {0} does not exist!", resource);
|
|
try (JsonReader reader = new JsonReader(new InputStreamReader(resourceStream))) {
|
|
reader.beginObject();
|
|
while (reader.hasNext()) map.put(reader.nextName(), (Map<String, Object>) readObject(reader));
|
|
reader.endObject();
|
|
}
|
|
} catch (IOException e) {
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
public static class Container<T extends ProtocolObject> {
|
|
// Maps do not need to be thread-safe as they are fully populated
|
|
// in the static initializer, should not be modified during runtime
|
|
|
|
// namespace -> registry data
|
|
private final Map<String, T> namespaceMap = new HashMap<>();
|
|
// id -> registry data
|
|
private final ObjectArray<T> ids = new ObjectArray<>();
|
|
private final Collection<T> objects = Collections.unmodifiableCollection(namespaceMap.values());
|
|
|
|
private final boolean initialized;
|
|
|
|
@ApiStatus.Internal
|
|
public Container(Resource resource, Loader<T> loader) {
|
|
for (var entry : Registry.load(resource).entrySet()) {
|
|
final String namespace = entry.getKey();
|
|
final Map<String, Object> object = entry.getValue();
|
|
loader.accept(this, namespace, object);
|
|
}
|
|
this.initialized = true;
|
|
this.ids.trim();
|
|
}
|
|
|
|
public T get(@NotNull String namespace) {
|
|
return namespaceMap.get(namespace);
|
|
}
|
|
|
|
public T getSafe(@NotNull String namespace) {
|
|
return get(namespace.contains(":") ? namespace : "minecraft:" + namespace);
|
|
}
|
|
|
|
public T getId(int id) {
|
|
return ids.get(id);
|
|
}
|
|
|
|
public Collection<T> values() {
|
|
return objects;
|
|
}
|
|
|
|
public void register(@NotNull T value) {
|
|
Check.stateCondition(initialized, "Registering is only available within the loader lambda.");
|
|
this.ids.set(value.id(), value);
|
|
this.namespaceMap.put(value.name(), value);
|
|
}
|
|
|
|
public interface Loader<T extends ProtocolObject> {
|
|
void accept(Container<T> container, String namespace, Map<String, Object> object);
|
|
}
|
|
}
|
|
|
|
@ApiStatus.Internal
|
|
public enum Resource {
|
|
BLOCKS("blocks.json"),
|
|
ITEMS("items.json"),
|
|
ENTITIES("entities.json"),
|
|
ENCHANTMENTS("enchantments.json"),
|
|
SOUNDS("sounds.json"),
|
|
STATISTICS("custom_statistics.json"),
|
|
POTION_EFFECTS("potion_effects.json"),
|
|
POTION_TYPES("potions.json"),
|
|
PARTICLES("particles.json"),
|
|
|
|
BLOCK_TAGS("tags/block_tags.json"),
|
|
ENTITY_TYPE_TAGS("tags/entity_type_tags.json"),
|
|
FLUID_TAGS("tags/fluid_tags.json"),
|
|
GAMEPLAY_TAGS("tags/gameplay_tags.json"),
|
|
ITEM_TAGS("tags/item_tags.json");
|
|
|
|
private final String name;
|
|
|
|
Resource(String name) {
|
|
this.name = name;
|
|
}
|
|
}
|
|
|
|
public static class BlockEntry extends Entry {
|
|
private final NamespaceID namespace;
|
|
private final int id;
|
|
private final int stateId;
|
|
private final String translationKey;
|
|
private final double hardness;
|
|
private final double explosionResistance;
|
|
private final double friction;
|
|
private final double speedFactor;
|
|
private final double jumpFactor;
|
|
private final boolean air;
|
|
private final boolean solid;
|
|
private final boolean liquid;
|
|
private final String blockEntity;
|
|
private final Supplier<Material> materialSupplier;
|
|
|
|
private BlockEntry(String namespace, Map<String, Object> main, Map<String, Object> override) {
|
|
super(main, override);
|
|
this.namespace = NamespaceID.from(namespace);
|
|
this.id = getInt("id");
|
|
this.stateId = getInt("stateId");
|
|
this.translationKey = getString("translationKey");
|
|
this.hardness = getDouble("hardness");
|
|
this.explosionResistance = getDouble("explosionResistance");
|
|
this.friction = getDouble("friction");
|
|
this.speedFactor = getDouble("speedFactor", 1);
|
|
this.jumpFactor = getDouble("jumpFactor", 1);
|
|
this.air = getBoolean("air", false);
|
|
this.solid = getBoolean("solid");
|
|
this.liquid = getBoolean("liquid", false);
|
|
|
|
{
|
|
Map<String, Object> blockEntity = element("blockEntity");
|
|
if (blockEntity != null) {
|
|
this.blockEntity = (String) blockEntity.get("namespace");
|
|
} else {
|
|
this.blockEntity = null;
|
|
}
|
|
}
|
|
{
|
|
final String materialNamespace = getString("correspondingItem", null);
|
|
this.materialSupplier = materialNamespace != null ? () -> Material.fromNamespaceId(materialNamespace) : () -> null;
|
|
}
|
|
}
|
|
|
|
public @NotNull NamespaceID namespace() {
|
|
return namespace;
|
|
}
|
|
|
|
public int id() {
|
|
return id;
|
|
}
|
|
|
|
public int stateId() {
|
|
return stateId;
|
|
}
|
|
|
|
public String translationKey() {
|
|
return translationKey;
|
|
}
|
|
|
|
public double hardness() {
|
|
return hardness;
|
|
}
|
|
|
|
public double explosionResistance() {
|
|
return explosionResistance;
|
|
}
|
|
|
|
public double friction() {
|
|
return friction;
|
|
}
|
|
|
|
public double speedFactor() {
|
|
return speedFactor;
|
|
}
|
|
|
|
public double jumpFactor() {
|
|
return jumpFactor;
|
|
}
|
|
|
|
public boolean isAir() {
|
|
return air;
|
|
}
|
|
|
|
public boolean isSolid() {
|
|
return solid;
|
|
}
|
|
|
|
public boolean isLiquid() {
|
|
return liquid;
|
|
}
|
|
|
|
public boolean isBlockEntity() {
|
|
return blockEntity != null;
|
|
}
|
|
|
|
public @Nullable String blockEntity() {
|
|
return blockEntity;
|
|
}
|
|
|
|
public @Nullable Material material() {
|
|
return materialSupplier.get();
|
|
}
|
|
}
|
|
|
|
public static class MaterialEntry extends Entry {
|
|
private final NamespaceID namespace;
|
|
private final int id;
|
|
private final String translationKey;
|
|
private final int maxStackSize;
|
|
private final int maxDamage;
|
|
private final boolean isFood;
|
|
private final Supplier<Block> blockSupplier;
|
|
private final EquipmentSlot equipmentSlot;
|
|
|
|
private MaterialEntry(String namespace, Map<String, Object> main, Map<String, Object> override) {
|
|
super(main, override);
|
|
this.namespace = NamespaceID.from(namespace);
|
|
this.id = getInt("id");
|
|
this.translationKey = getString("translationKey");
|
|
this.maxStackSize = getInt("maxStackSize", 64);
|
|
this.maxDamage = getInt("maxDamage", 0);
|
|
this.isFood = getBoolean("edible", false);
|
|
{
|
|
final String blockNamespace = getString("correspondingBlock", null);
|
|
this.blockSupplier = blockNamespace != null ? () -> Block.fromNamespaceId(blockNamespace) : () -> null;
|
|
}
|
|
|
|
{
|
|
final Map<String, Object> armorProperties = element("armorProperties");
|
|
if (armorProperties != null) {
|
|
final String slot = (String) armorProperties.get("slot");
|
|
switch (slot) {
|
|
case "feet" -> this.equipmentSlot = EquipmentSlot.BOOTS;
|
|
case "legs" -> this.equipmentSlot = EquipmentSlot.LEGGINGS;
|
|
case "chest" -> this.equipmentSlot = EquipmentSlot.CHESTPLATE;
|
|
case "head" -> this.equipmentSlot = EquipmentSlot.HELMET;
|
|
default -> this.equipmentSlot = null;
|
|
}
|
|
} else {
|
|
this.equipmentSlot = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public @NotNull NamespaceID namespace() {
|
|
return namespace;
|
|
}
|
|
|
|
public int id() {
|
|
return id;
|
|
}
|
|
|
|
public String translationKey() {
|
|
return translationKey;
|
|
}
|
|
|
|
public int maxStackSize() {
|
|
return maxStackSize;
|
|
}
|
|
|
|
public int maxDamage() {
|
|
return maxDamage;
|
|
}
|
|
|
|
public boolean isFood() {
|
|
return isFood;
|
|
}
|
|
|
|
public @Nullable Block block() {
|
|
return blockSupplier.get();
|
|
}
|
|
|
|
public boolean isArmor() {
|
|
return equipmentSlot != null;
|
|
}
|
|
|
|
public @Nullable EquipmentSlot equipmentSlot() {
|
|
return equipmentSlot;
|
|
}
|
|
}
|
|
|
|
public static class EntityEntry extends Entry {
|
|
private final NamespaceID namespace;
|
|
private final int id;
|
|
private final String translationKey;
|
|
private final double width;
|
|
private final double height;
|
|
private final double drag;
|
|
private final double acceleration;
|
|
private final EntitySpawnType spawnType;
|
|
|
|
private EntityEntry(String namespace, Map<String, Object> main, Map<String, Object> override) {
|
|
super(main, override);
|
|
this.namespace = NamespaceID.from(namespace);
|
|
this.id = getInt("id");
|
|
this.translationKey = getString("translationKey");
|
|
this.width = getDouble("width");
|
|
this.height = getDouble("height");
|
|
this.drag = getDouble("drag", 0.02);
|
|
this.acceleration = getDouble("acceleration", 0.08);
|
|
this.spawnType = EntitySpawnType.valueOf(getString("packetType").toUpperCase(Locale.ROOT));
|
|
}
|
|
|
|
public @NotNull NamespaceID namespace() {
|
|
return namespace;
|
|
}
|
|
|
|
public int id() {
|
|
return id;
|
|
}
|
|
|
|
public String translationKey() {
|
|
return translationKey;
|
|
}
|
|
|
|
public double width() {
|
|
return width;
|
|
}
|
|
|
|
public double height() {
|
|
return height;
|
|
}
|
|
|
|
public double drag() {
|
|
return drag;
|
|
}
|
|
|
|
public double acceleration() {
|
|
return acceleration;
|
|
}
|
|
|
|
public EntitySpawnType spawnType() {
|
|
return spawnType;
|
|
}
|
|
}
|
|
|
|
public static class EnchantmentEntry extends Entry {
|
|
private final NamespaceID namespace;
|
|
private final int id;
|
|
private final String translationKey;
|
|
private final double maxLevel;
|
|
private final boolean isCursed;
|
|
private final boolean isDiscoverable;
|
|
private final boolean isTradeable;
|
|
private final boolean isTreasureOnly;
|
|
|
|
private EnchantmentEntry(String namespace, Map<String, Object> main, Map<String, Object> override) {
|
|
super(main, override);
|
|
this.namespace = NamespaceID.from(namespace);
|
|
this.id = getInt("id");
|
|
this.translationKey = getString("translationKey");
|
|
this.maxLevel = getDouble("maxLevel");
|
|
this.isCursed = getBoolean("curse", false);
|
|
this.isDiscoverable = getBoolean("discoverable", true);
|
|
this.isTradeable = getBoolean("tradeable", true);
|
|
this.isTreasureOnly = getBoolean("treasureOnly", false);
|
|
}
|
|
|
|
public @NotNull NamespaceID namespace() {
|
|
return namespace;
|
|
}
|
|
|
|
public int id() {
|
|
return id;
|
|
}
|
|
|
|
public String translationKey() {
|
|
return translationKey;
|
|
}
|
|
|
|
public double maxLevel() {
|
|
return maxLevel;
|
|
}
|
|
|
|
public boolean isCursed() {
|
|
return isCursed;
|
|
}
|
|
|
|
public boolean isDiscoverable() {
|
|
return isDiscoverable;
|
|
}
|
|
|
|
public boolean isTradeable() {
|
|
return isTradeable;
|
|
}
|
|
|
|
public boolean isTreasureOnly() {
|
|
return isTreasureOnly;
|
|
}
|
|
}
|
|
|
|
public static class PotionEffectEntry extends Entry {
|
|
private final NamespaceID namespace;
|
|
private final int id;
|
|
private final String translationKey;
|
|
private final int color;
|
|
private final boolean isInstantaneous;
|
|
|
|
private PotionEffectEntry(String namespace, Map<String, Object> main, Map<String, Object> override) {
|
|
super(main, override);
|
|
this.namespace = NamespaceID.from(namespace);
|
|
this.id = getInt("id");
|
|
this.translationKey = getString("translationKey");
|
|
this.color = getInt("color");
|
|
this.isInstantaneous = getBoolean("instantaneous");
|
|
}
|
|
|
|
public @NotNull NamespaceID namespace() {
|
|
return namespace;
|
|
}
|
|
|
|
public int id() {
|
|
return id;
|
|
}
|
|
|
|
public String translationKey() {
|
|
return translationKey;
|
|
}
|
|
|
|
public int color() {
|
|
return color;
|
|
}
|
|
|
|
public boolean isInstantaneous() {
|
|
return isInstantaneous;
|
|
}
|
|
}
|
|
|
|
public static class Entry {
|
|
private final Map<String, Object> main, override;
|
|
|
|
private Entry(Map<String, Object> main, Map<String, Object> override) {
|
|
this.main = main;
|
|
this.override = override;
|
|
}
|
|
|
|
public String getString(String name, String defaultValue) {
|
|
var element = element(name);
|
|
return element != null ? (String) element : defaultValue;
|
|
}
|
|
|
|
public String getString(String name) {
|
|
return element(name);
|
|
}
|
|
|
|
public double getDouble(String name, double defaultValue) {
|
|
var element = element(name);
|
|
return element != null ? ((Number) element).doubleValue() : defaultValue;
|
|
}
|
|
|
|
public double getDouble(String name) {
|
|
return ((Number) element(name)).doubleValue();
|
|
}
|
|
|
|
public int getInt(String name, int defaultValue) {
|
|
var element = element(name);
|
|
return element != null ? ((Number) element).intValue() : defaultValue;
|
|
}
|
|
|
|
public int getInt(String name) {
|
|
return ((Number) element(name)).intValue();
|
|
}
|
|
|
|
public boolean getBoolean(String name, boolean defaultValue) {
|
|
var element = element(name);
|
|
return element != null ? (boolean) element : defaultValue;
|
|
}
|
|
|
|
public boolean getBoolean(String name) {
|
|
return element(name);
|
|
}
|
|
|
|
protected <T> T element(String name) {
|
|
Object result;
|
|
if (override != null && (result = override.get(name)) != null) {
|
|
return (T) result;
|
|
}
|
|
return (T) main.get(name);
|
|
}
|
|
}
|
|
|
|
private static Object readObject(JsonReader reader) throws IOException {
|
|
return switch (reader.peek()) {
|
|
case BEGIN_ARRAY -> {
|
|
ObjectArrayList<Object> list = new ObjectArrayList<>();
|
|
reader.beginArray();
|
|
while (reader.hasNext()) list.add(readObject(reader));
|
|
reader.endArray();
|
|
yield new ObjectArrayList<>(list);
|
|
}
|
|
case BEGIN_OBJECT -> {
|
|
Object2ObjectArrayMap<String, Object> map = new Object2ObjectArrayMap<>();
|
|
reader.beginObject();
|
|
while (reader.hasNext()) map.put(reader.nextName().intern(), readObject(reader));
|
|
reader.endObject();
|
|
yield new Object2ObjectArrayMap<>(map);
|
|
}
|
|
case STRING -> reader.nextString().intern();
|
|
case NUMBER -> ToNumberPolicy.LONG_OR_DOUBLE.readNumber(reader);
|
|
case BOOLEAN -> reader.nextBoolean();
|
|
default -> throw new IllegalStateException("Invalid peek: " + reader.peek());
|
|
};
|
|
}
|
|
}
|