All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.minestom.server.registry.DynamicRegistryImpl Maven / Gradle / Ivy

There is a newer version: 7320437640
Show newest version
package net.minestom.server.registry;

import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.TagStringIOExt;
import net.minestom.server.MinecraftServer;
import net.minestom.server.ServerFlag;
import net.minestom.server.gamedata.DataPack;
import net.minestom.server.network.packet.server.CachedPacket;
import net.minestom.server.network.packet.server.SendablePacket;
import net.minestom.server.network.packet.server.configuration.RegistryDataPacket;
import net.minestom.server.utils.NamespaceID;
import net.minestom.server.utils.nbt.BinaryTagSerializer;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;

@ApiStatus.Internal
final class DynamicRegistryImpl implements DynamicRegistry {
    private static final UnsupportedOperationException UNSAFE_REMOVE_EXCEPTION = new UnsupportedOperationException("Unsafe remove is disabled. Enable by setting the system property 'minestom.registry.unsafe-ops' to 'true'");

    record KeyImpl(@NotNull NamespaceID namespace) implements Key {

        @Override
        public String toString() {
            return namespace.asString();
        }

        @Override
        public int hashCode() {
            return namespace.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            KeyImpl key = (KeyImpl) obj;
            return namespace.equals(key.namespace);
        }
    }

    private final CachedPacket vanillaRegistryDataPacket = new CachedPacket(() -> createRegistryDataPacket(true));

    private final ReentrantLock lock = new ReentrantLock(); // Protects writes
    private final List entryById = new CopyOnWriteArrayList<>();
    private final Map entryByName = new ConcurrentHashMap<>();
    private final List idByName = new CopyOnWriteArrayList<>();
    private final List packById = new CopyOnWriteArrayList<>();

    private final String id;
    private final BinaryTagSerializer nbtType;

    DynamicRegistryImpl(@NotNull String id, @Nullable BinaryTagSerializer nbtType) {
        this.id = id;
        this.nbtType = nbtType;
    }

    @Override
    public @NotNull String id() {
        return id;
    }

    public @UnknownNullability BinaryTagSerializer nbtType() {
        return nbtType;
    }

    @Override
    public @Nullable T get(int id) {
        if (id < 0 || id >= entryById.size()) {
            return null;
        }
        return entryById.get(id);
    }

    @Override
    public @Nullable T get(@NotNull NamespaceID namespace) {
        return entryByName.get(namespace);
    }

    @Override
    public @Nullable Key getKey(@NotNull T value) {
        int index = entryById.indexOf(value);
        return index == -1 ? null : getKey(index);
    }

    @Override
    public @Nullable Key getKey(int id) {
        if (id < 0 || id >= entryById.size())
            return null;
        return Key.of(idByName.get(id));
    }

    @Override
    public @Nullable NamespaceID getName(int id) {
        if (id < 0 || id >= entryById.size())
            return null;
        return idByName.get(id);
    }

    @Override
    public @Nullable DataPack getPack(int id) {
        if (id < 0 || id >= packById.size())
            return null;
        return packById.get(id);
    }

    @Override
    public int getId(@NotNull NamespaceID id) {
        return idByName.indexOf(id);
    }

    @Override
    public @NotNull List values() {
        return Collections.unmodifiableList(entryById);
    }

    @Override
    public @NotNull DynamicRegistry.Key register(@NotNull NamespaceID namespaceId, @NotNull T object, @Nullable DataPack pack) {
        // This check is disabled in tests because we remake server processes over and over.
        // todo: re-enable this check
//        Check.stateCondition((!DebugUtils.INSIDE_TEST && MinecraftServer.process() != null && !MinecraftServer.isStarted()) && !ServerFlag.REGISTRY_LATE_REGISTER,
//                "Registering an object to a dynamic registry ({0}) after the server is started can lead to " +
//                        "registry desync between the client and server. This is usually unwanted behavior. If you " +
//                        "know what you're doing and would like this behavior, set the `minestom.registry.late-register` " +
//                        "system property.", id);

        lock.lock();
        try {
            int id = idByName.indexOf(namespaceId);
            if (id == -1) id = entryById.size();

            entryById.add(id, object);
            entryByName.put(namespaceId, object);
            idByName.add(namespaceId);
            packById.add(id, pack);
            vanillaRegistryDataPacket.invalidate();
            return Key.of(namespaceId);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public boolean remove(@NotNull NamespaceID namespaceId) throws UnsupportedOperationException {
        if (!ServerFlag.REGISTRY_UNSAFE_OPS) throw UNSAFE_REMOVE_EXCEPTION;

        lock.lock();
        try {
            int id = idByName.indexOf(namespaceId);
            if (id == -1) return false;

            entryById.remove(id);
            entryByName.remove(namespaceId);
            idByName.remove(id);
            packById.remove(id);
            vanillaRegistryDataPacket.invalidate();
            return true;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public @NotNull SendablePacket registryDataPacket(boolean excludeVanilla) {
        // We cache the vanilla packet because that is by far the most common case. If some client claims not to have
        // the vanilla datapack we can compute the entire thing.
        return excludeVanilla ? vanillaRegistryDataPacket : createRegistryDataPacket(false);
    }

    private @NotNull RegistryDataPacket createRegistryDataPacket(boolean excludeVanilla) {
        Check.notNull(nbtType, "Cannot create registry data packet for server-only registry");
        BinaryTagSerializer.Context context = new BinaryTagSerializer.ContextWithRegistries(MinecraftServer.process(), true);
        List entries = new ArrayList<>(entryById.size());
        for (int i = 0; i < entryById.size(); i++) {
            CompoundBinaryTag data = null;
            // sorta todo, sorta just a note:
            // Right now we very much only support the minecraft:core (vanilla) 'pack'. Any entry which was not loaded
            // from static data will be treated as non vanilla and always sent completely. However, we really should
            // support arbitrary packs and associate all registry data with a datapack. Additionally, we should generate
            // all data for the experimental datapacks built in to vanilla such as the next update experimental (1.21 at
            // the time of writing). Datagen currently behaves kind of badly in that the registry inspecting generators
            // like material, block, etc generate entries which are behind feature flags, whereas the ones which inspect
            // static assets (the traditionally dynamic registries), do not generate those assets.
            T entry = entryById.get(i);
            DataPack pack = packById.get(i);
            if (!excludeVanilla || pack != DataPack.MINECRAFT_CORE) {
                data = (CompoundBinaryTag) nbtType.write(context, entry);
            }
            //noinspection DataFlowIssue
            entries.add(new RegistryDataPacket.Entry(getKey(i).name(), data));
        }
        return new RegistryDataPacket(id, entries);
    }

    static  void loadStaticRegistry(@NotNull DynamicRegistry registry, @NotNull Registry.Resource resource, @NotNull Registry.Container.Loader loader, @Nullable Comparator idComparator) {
        List>> entries = new ArrayList<>(Registry.load(resource).entrySet());
        if (idComparator != null) entries.sort(Map.Entry.comparingByKey(idComparator));
        for (var entry : entries) {
            final String namespace = entry.getKey();
            final Registry.Properties properties = Registry.Properties.fromMap(entry.getValue());
            registry.register(namespace, loader.get(namespace, properties), DataPack.MINECRAFT_CORE);
        }
    }

    static  void loadStaticSnbtRegistry(@NotNull Registries registries, @NotNull DynamicRegistryImpl registry, @NotNull Registry.Resource resource) {
        Check.argCondition(!resource.fileName().endsWith(".snbt"), "Resource must be an SNBT file: {0}", resource.fileName());
        try (InputStream resourceStream = Registry.class.getClassLoader().getResourceAsStream(resource.fileName())) {
            Check.notNull(resourceStream, "Resource {0} does not exist!", resource);
            final BinaryTag tag = TagStringIOExt.readTag(new String(resourceStream.readAllBytes(), StandardCharsets.UTF_8));
            if (!(tag instanceof CompoundBinaryTag compound)) {
                throw new IllegalStateException("Root tag must be a compound tag");
            }

            final BinaryTagSerializer.Context context = new BinaryTagSerializer.ContextWithRegistries(registries, false);
            for (Map.Entry entry : compound) {
                final String namespace = entry.getKey();
                final T value = registry.nbtType.read(context, entry.getValue());
                Check.notNull(value, "Failed to read value for namespace {0}", namespace);
                registry.register(namespace, value, DataPack.MINECRAFT_CORE);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy