aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0993-Registry-Modification-API.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/0993-Registry-Modification-API.patch')
-rw-r--r--patches/server/0993-Registry-Modification-API.patch1499
1 files changed, 1499 insertions, 0 deletions
diff --git a/patches/server/0993-Registry-Modification-API.patch b/patches/server/0993-Registry-Modification-API.patch
new file mode 100644
index 0000000000..ac28f625c7
--- /dev/null
+++ b/patches/server/0993-Registry-Modification-API.patch
@@ -0,0 +1,1499 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <[email protected]>
+Date: Mon, 27 Feb 2023 18:28:39 -0800
+Subject: [PATCH] Registry Modification API
+
+== AT ==
+public net.minecraft.core.MappedRegistry validateWrite(Lnet/minecraft/resources/ResourceKey;)V
+public net.minecraft.resources.RegistryOps lookupProvider
+public net.minecraft.resources.RegistryOps$HolderLookupAdapter
+
+diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistries.java b/src/main/java/io/papermc/paper/registry/PaperRegistries.java
+index 633b01431750d4b40159a57bf25fb35c6670ff1b..5cf598905ed6a7ac2b0d9ced3420adaf20ceb6af 100644
+--- a/src/main/java/io/papermc/paper/registry/PaperRegistries.java
++++ b/src/main/java/io/papermc/paper/registry/PaperRegistries.java
+@@ -2,6 +2,7 @@ package io.papermc.paper.registry;
+
+ import io.papermc.paper.adventure.PaperAdventure;
+ import io.papermc.paper.registry.entry.RegistryEntry;
++import io.papermc.paper.registry.tag.TagKey;
+ import java.util.Collections;
+ import java.util.IdentityHashMap;
+ import java.util.List;
+@@ -58,6 +59,7 @@ import org.checkerframework.framework.qual.DefaultQualifier;
+
+ import static io.papermc.paper.registry.entry.RegistryEntry.apiOnly;
+ import static io.papermc.paper.registry.entry.RegistryEntry.entry;
++import static io.papermc.paper.registry.entry.RegistryEntry.writable;
+
+ @DefaultQualifier(NonNull.class)
+ public final class PaperRegistries {
+@@ -141,6 +143,15 @@ public final class PaperRegistries {
+ return ResourceKey.create((ResourceKey<? extends Registry<M>>) PaperRegistries.registryToNms(typedKey.registryKey()), PaperAdventure.asVanilla(typedKey.key()));
+ }
+
++ public static <M, T> TagKey<T> fromNms(final net.minecraft.tags.TagKey<M> tagKey) {
++ return TagKey.create(registryFromNms(tagKey.registry()), CraftNamespacedKey.fromMinecraft(tagKey.location()));
++ }
++
++ @SuppressWarnings({"unchecked", "RedundantCast"})
++ public static <M, T> net.minecraft.tags.TagKey<M> toNms(final TagKey<T> tagKey) {
++ return net.minecraft.tags.TagKey.create((ResourceKey<? extends Registry<M>>) registryToNms(tagKey.registryKey()), PaperAdventure.asVanilla(tagKey.key()));
++ }
++
+ private PaperRegistries() {
+ }
+ }
+diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java b/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java
+index 35b6a7c5bac9640332a833bd3627f2bcb1bbf2f3..1026b9c04f94ed73049b980822a2eafdbacea7fd 100644
+--- a/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java
++++ b/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java
+@@ -83,6 +83,14 @@ public class PaperRegistryAccess implements RegistryAccess {
+ return possiblyUnwrap(registryHolder.get());
+ }
+
++ public <M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> WritableCraftRegistry<M, T, B> getWritableRegistry(final RegistryKey<T> key) {
++ final Registry<T> registry = this.getRegistry(key);
++ if (registry instanceof WritableCraftRegistry<?, T, ?>) {
++ return (WritableCraftRegistry<M, T, B>) registry;
++ }
++ throw new IllegalArgumentException(key + " does not point to a writable registry");
++ }
++
+ private static <T extends Keyed> Registry<T> possiblyUnwrap(final Registry<T> registry) {
+ if (registry instanceof final DelayedRegistry<T, ?> delayedRegistry) { // if not coming from legacy, unwrap the delayed registry
+ return delayedRegistry.delegate();
+diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistryBuilder.java b/src/main/java/io/papermc/paper/registry/PaperRegistryBuilder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..528c6ee1739d92f766f3904acd7fc5734c93388a
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/PaperRegistryBuilder.java
+@@ -0,0 +1,26 @@
++package io.papermc.paper.registry;
++
++import io.papermc.paper.registry.data.util.Conversions;
++import net.minecraft.resources.RegistryOps;
++import org.checkerframework.checker.nullness.qual.Nullable;
++
++public interface PaperRegistryBuilder<M, T> extends RegistryBuilder<T> {
++
++ M build();
++
++ @FunctionalInterface
++ interface Filler<M, T, B extends PaperRegistryBuilder<M, T>> {
++
++ B fill(Conversions conversions, TypedKey<T> key, @Nullable M nms);
++
++ default Factory<M, T, B> asFactory() {
++ return (lookup, key) -> this.fill(lookup, key, null);
++ }
++ }
++
++ @FunctionalInterface
++ interface Factory<M, T, B extends PaperRegistryBuilder<M, T>> {
++
++ B create(Conversions conversions, TypedKey<T> key);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistryListenerManager.java b/src/main/java/io/papermc/paper/registry/PaperRegistryListenerManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..69e946173407eb05b18a2b19b0d45cbb3213570b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/PaperRegistryListenerManager.java
+@@ -0,0 +1,183 @@
++package io.papermc.paper.registry;
++
++import com.google.common.base.Preconditions;
++import com.mojang.serialization.Lifecycle;
++import io.papermc.paper.plugin.bootstrap.BootstrapContext;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner;
++import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType;
++import io.papermc.paper.registry.data.util.Conversions;
++import io.papermc.paper.registry.entry.RegistryEntry;
++import io.papermc.paper.registry.entry.RegistryEntryInfo;
++import io.papermc.paper.registry.event.RegistryEntryAddEventImpl;
++import io.papermc.paper.registry.event.RegistryEventMap;
++import io.papermc.paper.registry.event.RegistryEventProvider;
++import io.papermc.paper.registry.event.RegistryFreezeEvent;
++import io.papermc.paper.registry.event.RegistryFreezeEventImpl;
++import io.papermc.paper.registry.event.type.RegistryEntryAddEventType;
++import io.papermc.paper.registry.event.type.RegistryEntryAddEventTypeImpl;
++import io.papermc.paper.registry.event.type.RegistryLifecycleEventType;
++import java.util.Optional;
++import net.kyori.adventure.key.Key;
++import net.minecraft.core.Holder;
++import net.minecraft.core.MappedRegistry;
++import net.minecraft.core.RegistrationInfo;
++import net.minecraft.core.Registry;
++import net.minecraft.core.WritableRegistry;
++import net.minecraft.core.registries.BuiltInRegistries;
++import net.minecraft.resources.ResourceKey;
++import net.minecraft.resources.ResourceLocation;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.intellij.lang.annotations.Subst;
++
++public class PaperRegistryListenerManager {
++
++ public static final PaperRegistryListenerManager INSTANCE = new PaperRegistryListenerManager();
++
++ public final RegistryEventMap valueAddEventTypes = new RegistryEventMap("value add");
++ public final RegistryEventMap freezeEventTypes = new RegistryEventMap("freeze");
++
++ private PaperRegistryListenerManager() {
++ }
++
++ /**
++ * For {@link Registry#register(Registry, String, Object)}
++ */
++ public <M> M registerWithListeners(final Registry<M> registry, final String id, final M nms) {
++ return this.registerWithListeners(registry, ResourceLocation.withDefaultNamespace(id), nms);
++ }
++
++ /**
++ * For {@link Registry#register(Registry, ResourceLocation, Object)}
++ */
++ public <M> M registerWithListeners(final Registry<M> registry, final ResourceLocation loc, final M nms) {
++ return this.registerWithListeners(registry, ResourceKey.create(registry.key(), loc), nms);
++ }
++
++ /**
++ * For {@link Registry#register(Registry, ResourceKey, Object)}
++ */
++ public <M> M registerWithListeners(final Registry<M> registry, final ResourceKey<M> key, final M nms) {
++ return this.registerWithListeners(registry, key, nms, RegistrationInfo.BUILT_IN, PaperRegistryListenerManager::registerWithInstance, BuiltInRegistries.BUILT_IN_CONVERSIONS);
++ }
++
++ /**
++ * For {@link Registry#registerForHolder(Registry, ResourceLocation, Object)}
++ */
++ public <M> Holder.Reference<M> registerForHolderWithListeners(final Registry<M> registry, final ResourceLocation loc, final M nms) {
++ return this.registerForHolderWithListeners(registry, ResourceKey.create(registry.key(), loc), nms);
++ }
++
++ /**
++ * For {@link Registry#registerForHolder(Registry, ResourceKey, Object)}
++ */
++ public <M> Holder.Reference<M> registerForHolderWithListeners(final Registry<M> registry, final ResourceKey<M> key, final M nms) {
++ return this.registerWithListeners(registry, key, nms, RegistrationInfo.BUILT_IN, WritableRegistry::register, BuiltInRegistries.BUILT_IN_CONVERSIONS);
++ }
++
++ public <M> void registerWithListeners(
++ final Registry<M> registry,
++ final ResourceKey<M> key,
++ final M nms,
++ final RegistrationInfo registrationInfo,
++ final Conversions conversions
++ ) {
++ this.registerWithListeners(registry, key, nms, registrationInfo, WritableRegistry::register, conversions);
++ }
++
++ // TODO remove Keyed
++ public <M, T extends org.bukkit.Keyed, B extends PaperRegistryBuilder<M, T>, R> R registerWithListeners(
++ final Registry<M> registry,
++ final ResourceKey<M> key,
++ final M nms,
++ final RegistrationInfo registrationInfo,
++ final RegisterMethod<M, R> registerMethod,
++ final Conversions conversions
++ ) {
++ Preconditions.checkState(LaunchEntryPointHandler.INSTANCE.hasEntered(Entrypoint.BOOTSTRAPPER), registry.key() + " tried to run modification listeners before bootstrappers have been called"); // verify that bootstrappers have been called
++ final @Nullable RegistryEntryInfo<M, T> entry = PaperRegistries.getEntry(registry.key());
++ if (!RegistryEntry.Modifiable.isModifiable(entry) || !this.valueAddEventTypes.hasHandlers(entry.apiKey())) {
++ return registerMethod.register((WritableRegistry<M>) registry, key, nms, registrationInfo);
++ }
++ final RegistryEntry.Modifiable<M, T, B> modifiableEntry = RegistryEntry.Modifiable.asModifiable(entry);
++ @SuppressWarnings("PatternValidation") final TypedKey<T> typedKey = TypedKey.create(entry.apiKey(), Key.key(key.location().getNamespace(), key.location().getPath()));
++ final B builder = modifiableEntry.fillBuilder(conversions, typedKey, nms);
++ return this.registerWithListeners(registry, modifiableEntry, key, nms, builder, registrationInfo, registerMethod, conversions);
++ }
++
++ <M, T extends org.bukkit.Keyed, B extends PaperRegistryBuilder<M, T>> void registerWithListeners( // TODO remove Keyed
++ final WritableRegistry<M> registry,
++ final RegistryEntryInfo<M, T> entry,
++ final ResourceKey<M> key,
++ final B builder,
++ final RegistrationInfo registrationInfo,
++ final Conversions conversions
++ ) {
++ if (!RegistryEntry.Modifiable.isModifiable(entry) || !this.valueAddEventTypes.hasHandlers(entry.apiKey())) {
++ registry.register(key, builder.build(), registrationInfo);
++ return;
++ }
++ this.registerWithListeners(registry, RegistryEntry.Modifiable.asModifiable(entry), key, null, builder, registrationInfo, WritableRegistry::register, conversions);
++ }
++
++ public <M, T extends org.bukkit.Keyed, B extends PaperRegistryBuilder<M, T>, R> R registerWithListeners( // TODO remove Keyed
++ final Registry<M> registry,
++ final RegistryEntry.Modifiable<M, T, B> entry,
++ final ResourceKey<M> key,
++ final @Nullable M oldNms,
++ final B builder,
++ RegistrationInfo registrationInfo,
++ final RegisterMethod<M, R> registerMethod,
++ final Conversions conversions
++ ) {
++ @Subst("namespace:key") final ResourceLocation beingAdded = key.location();
++ @SuppressWarnings("PatternValidation") final TypedKey<T> typedKey = TypedKey.create(entry.apiKey(), Key.key(beingAdded.getNamespace(), beingAdded.getPath()));
++ final RegistryEntryAddEventImpl<T, B> event = entry.createEntryAddEvent(typedKey, builder, conversions);
++ LifecycleEventRunner.INSTANCE.callEvent(this.valueAddEventTypes.getEventType(entry.apiKey()), event);
++ if (oldNms != null) {
++ ((MappedRegistry<M>) registry).clearIntrusiveHolder(oldNms);
++ }
++ final M newNms = event.builder().build();
++ if (oldNms != null && !newNms.equals(oldNms)) {
++ registrationInfo = new RegistrationInfo(Optional.empty(), Lifecycle.experimental());
++ }
++ return registerMethod.register((WritableRegistry<M>) registry, key, newNms, registrationInfo);
++ }
++
++ private static <M> M registerWithInstance(final WritableRegistry<M> writableRegistry, final ResourceKey<M> key, final M value, final RegistrationInfo registrationInfo) {
++ writableRegistry.register(key, value, registrationInfo);
++ return value;
++ }
++
++ @FunctionalInterface
++ public interface RegisterMethod<M, R> {
++
++ R register(WritableRegistry<M> writableRegistry, ResourceKey<M> key, M value, RegistrationInfo registrationInfo);
++ }
++
++ public <M, T extends org.bukkit.Keyed, B extends PaperRegistryBuilder<M, T>> void runFreezeListeners(final ResourceKey<? extends Registry<M>> resourceKey, final Conversions conversions) {
++ final @Nullable RegistryEntryInfo<M, T> entry = PaperRegistries.getEntry(resourceKey);
++ if (!RegistryEntry.Addable.isAddable(entry) || !this.freezeEventTypes.hasHandlers(entry.apiKey())) {
++ return;
++ }
++ final RegistryEntry.Addable<M, T, B> writableEntry = RegistryEntry.Addable.asAddable(entry);
++ final WritableCraftRegistry<M, T, B> writableRegistry = PaperRegistryAccess.instance().getWritableRegistry(entry.apiKey());
++ final RegistryFreezeEventImpl<T, B> event = writableEntry.createFreezeEvent(writableRegistry, conversions);
++ LifecycleEventRunner.INSTANCE.callEvent(this.freezeEventTypes.getEventType(entry.apiKey()), event);
++ }
++
++ public <T, B extends RegistryBuilder<T>> RegistryEntryAddEventType<T, B> getRegistryValueAddEventType(final RegistryEventProvider<T, B> type) {
++ if (!RegistryEntry.Modifiable.isModifiable(PaperRegistries.getEntry(type.registryKey()))) {
++ throw new IllegalArgumentException(type.registryKey() + " does not support RegistryEntryAddEvent");
++ }
++ return this.valueAddEventTypes.getOrCreate(type.registryKey(), RegistryEntryAddEventTypeImpl::new);
++ }
++
++ public <T, B extends RegistryBuilder<T>> LifecycleEventType.Prioritizable<BootstrapContext, RegistryFreezeEvent<T, B>> getRegistryFreezeEventType(final RegistryEventProvider<T, B> type) {
++ if (!RegistryEntry.Addable.isAddable(PaperRegistries.getEntry(type.registryKey()))) {
++ throw new IllegalArgumentException(type.registryKey() + " does not support RegistryFreezeEvent");
++ }
++ return this.freezeEventTypes.getOrCreate(type.registryKey(), RegistryLifecycleEventType::new);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/WritableCraftRegistry.java b/src/main/java/io/papermc/paper/registry/WritableCraftRegistry.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..78317c7ab42a666f19634593a8f3b696700764c8
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/WritableCraftRegistry.java
+@@ -0,0 +1,92 @@
++package io.papermc.paper.registry;
++
++import com.mojang.serialization.Lifecycle;
++import io.papermc.paper.adventure.PaperAdventure;
++import io.papermc.paper.registry.data.util.Conversions;
++import io.papermc.paper.registry.entry.RegistryEntry;
++import io.papermc.paper.registry.event.WritableRegistry;
++import java.util.Optional;
++import java.util.function.BiFunction;
++import java.util.function.Consumer;
++import net.minecraft.core.MappedRegistry;
++import net.minecraft.core.RegistrationInfo;
++import net.minecraft.resources.ResourceKey;
++import org.bukkit.Keyed;
++import org.bukkit.NamespacedKey;
++import org.bukkit.craftbukkit.CraftRegistry;
++import org.bukkit.craftbukkit.util.ApiVersion;
++import org.checkerframework.checker.nullness.qual.Nullable;
++
++public class WritableCraftRegistry<M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> extends CraftRegistry<T, M> {
++
++ private static final RegistrationInfo FROM_PLUGIN = new RegistrationInfo(Optional.empty(), Lifecycle.experimental());
++
++ private final RegistryEntry.BuilderHolder<M, T, B> entry;
++ private final MappedRegistry<M> registry;
++ private final PaperRegistryBuilder.Factory<M, T, ? extends B> builderFactory;
++ private final BiFunction<? super NamespacedKey, M, T> minecraftToBukkit;
++
++ public WritableCraftRegistry(
++ final RegistryEntry.BuilderHolder<M, T, B> entry,
++ final Class<?> classToPreload,
++ final MappedRegistry<M> registry,
++ final BiFunction<NamespacedKey, ApiVersion, NamespacedKey> serializationUpdater,
++ final PaperRegistryBuilder.Factory<M, T, ? extends B> builderFactory,
++ final BiFunction<? super NamespacedKey, M, T> minecraftToBukkit
++ ) {
++ super(classToPreload, registry, null, serializationUpdater);
++ this.entry = entry;
++ this.registry = registry;
++ this.builderFactory = builderFactory;
++ this.minecraftToBukkit = minecraftToBukkit;
++ }
++
++ public void register(final TypedKey<T> key, final Consumer<? super B> value, final Conversions conversions) {
++ final ResourceKey<M> resourceKey = ResourceKey.create(this.registry.key(), PaperAdventure.asVanilla(key.key()));
++ this.registry.validateWrite(resourceKey);
++ final B builder = this.newBuilder(conversions, key);
++ value.accept(builder);
++ PaperRegistryListenerManager.INSTANCE.registerWithListeners(
++ this.registry,
++ RegistryEntry.Modifiable.asModifiable(this.entry),
++ resourceKey,
++ builder,
++ FROM_PLUGIN,
++ conversions
++ );
++ }
++
++ @Override
++ public final @Nullable T createBukkit(final NamespacedKey namespacedKey, final @Nullable M minecraft) {
++ if (minecraft == null) {
++ return null;
++ }
++ return this.minecraftToBukkit(namespacedKey, minecraft);
++ }
++
++ public WritableRegistry<T, B> createApiWritableRegistry(final Conversions conversions) {
++ return new ApiWritableRegistry(conversions);
++ }
++
++ public T minecraftToBukkit(final NamespacedKey namespacedKey, final M minecraft) {
++ return this.minecraftToBukkit.apply(namespacedKey, minecraft);
++ }
++
++ protected B newBuilder(final Conversions conversions, final TypedKey<T> key) {
++ return this.builderFactory.create(conversions, key);
++ }
++
++ public class ApiWritableRegistry implements WritableRegistry<T, B> {
++
++ private final Conversions conversions;
++
++ public ApiWritableRegistry(final Conversions conversions) {
++ this.conversions = conversions;
++ }
++
++ @Override
++ public void register(final TypedKey<T> key, final Consumer<? super B> value) {
++ WritableCraftRegistry.this.register(key, value, this.conversions);
++ }
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/data/util/Conversions.java b/src/main/java/io/papermc/paper/registry/data/util/Conversions.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..eda5cc7d45ef59ccc1c9c7e027c1f044f1dcc86b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/data/util/Conversions.java
+@@ -0,0 +1,36 @@
++package io.papermc.paper.registry.data.util;
++
++import com.mojang.serialization.JavaOps;
++import io.papermc.paper.adventure.WrapperAwareSerializer;
++import net.kyori.adventure.text.Component;
++import net.minecraft.resources.RegistryOps;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.jetbrains.annotations.Contract;
++
++@DefaultQualifier(NonNull.class)
++public class Conversions {
++
++ private final RegistryOps.RegistryInfoLookup lookup;
++ private final WrapperAwareSerializer serializer;
++
++ public Conversions(final RegistryOps.RegistryInfoLookup lookup) {
++ this.lookup = lookup;
++ this.serializer = new WrapperAwareSerializer(() -> RegistryOps.create(JavaOps.INSTANCE, lookup));
++ }
++
++ public RegistryOps.RegistryInfoLookup lookup() {
++ return this.lookup;
++ }
++
++ @Contract("null -> null; !null -> !null")
++ public net.minecraft.network.chat.@Nullable Component asVanilla(final @Nullable Component adventure) {
++ if (adventure == null) return null;
++ return this.serializer.serialize(adventure);
++ }
++
++ public Component asAdventure(final net.minecraft.network.chat.@Nullable Component vanilla) {
++ return vanilla == null ? Component.empty() : this.serializer.deserialize(vanilla);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/entry/AddableRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/AddableRegistryEntry.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..aeec9b3ae2911f041d000b3db72f37974020ba60
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/entry/AddableRegistryEntry.java
+@@ -0,0 +1,44 @@
++package io.papermc.paper.registry.entry;
++
++import io.papermc.paper.registry.PaperRegistryBuilder;
++import io.papermc.paper.registry.RegistryHolder;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.TypedKey;
++import io.papermc.paper.registry.WritableCraftRegistry;
++import io.papermc.paper.registry.data.util.Conversions;
++import java.util.function.BiFunction;
++import net.minecraft.core.MappedRegistry;
++import net.minecraft.core.Registry;
++import net.minecraft.resources.ResourceKey;
++import org.bukkit.Keyed;
++import org.bukkit.NamespacedKey;
++
++public class AddableRegistryEntry<M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> extends CraftRegistryEntry<M, T> implements RegistryEntry.Addable<M, T, B> {
++
++ private final PaperRegistryBuilder.Filler<M, T, B> builderFiller;
++
++ protected AddableRegistryEntry(
++ final ResourceKey<? extends Registry<M>> mcKey,
++ final RegistryKey<T> apiKey,
++ final Class<?> classToPreload,
++ final BiFunction<NamespacedKey, M, T> minecraftToBukkit,
++ final PaperRegistryBuilder.Filler<M, T, B> builderFiller
++ ) {
++ super(mcKey, apiKey, classToPreload, minecraftToBukkit);
++ this.builderFiller = builderFiller;
++ }
++
++ private WritableCraftRegistry<M, T, B> createRegistry(final Registry<M> registry) {
++ return new WritableCraftRegistry<>(this, this.classToPreload, (MappedRegistry<M>) registry, this.updater, this.builderFiller.asFactory(), this.minecraftToBukkit);
++ }
++
++ @Override
++ public RegistryHolder<T> createRegistryHolder(final Registry<M> nmsRegistry) {
++ return new RegistryHolder.Memoized<>(() -> this.createRegistry(nmsRegistry));
++ }
++
++ @Override
++ public B fillBuilder(final Conversions conversions, final TypedKey<T> key, final M nms) {
++ return this.builderFiller.fill(conversions, key, nms);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/entry/ModifiableRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/ModifiableRegistryEntry.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..515a995e3862f8e7cb93d149315ea32e04a08716
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/entry/ModifiableRegistryEntry.java
+@@ -0,0 +1,32 @@
++package io.papermc.paper.registry.entry;
++
++import io.papermc.paper.registry.PaperRegistryBuilder;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.TypedKey;
++import io.papermc.paper.registry.data.util.Conversions;
++import java.util.function.BiFunction;
++import net.minecraft.core.Registry;
++import net.minecraft.resources.ResourceKey;
++import org.bukkit.Keyed;
++import org.bukkit.NamespacedKey;
++
++public class ModifiableRegistryEntry<M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> extends CraftRegistryEntry<M, T> implements RegistryEntry.Modifiable<M, T, B> {
++
++ protected final PaperRegistryBuilder.Filler<M, T, B> builderFiller;
++
++ protected ModifiableRegistryEntry(
++ final ResourceKey<? extends Registry<M>> mcKey,
++ final RegistryKey<T> apiKey,
++ final Class<?> toPreload,
++ final BiFunction<NamespacedKey, M, T> minecraftToBukkit,
++ final PaperRegistryBuilder.Filler<M, T, B> builderFiller
++ ) {
++ super(mcKey, apiKey, toPreload, minecraftToBukkit);
++ this.builderFiller = builderFiller;
++ }
++
++ @Override
++ public B fillBuilder(final Conversions conversions, final TypedKey<T> key, final M nms) {
++ return this.builderFiller.fill(conversions, key, nms);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java
+index 15991bf13894d850f360a520d1815711d25973ec..f2e919705301cb23ed1938ca3c1976378249172c 100644
+--- a/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java
++++ b/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java
+@@ -1,7 +1,13 @@
+ package io.papermc.paper.registry.entry;
+
++import io.papermc.paper.registry.PaperRegistryBuilder;
+ import io.papermc.paper.registry.RegistryHolder;
+ import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.TypedKey;
++import io.papermc.paper.registry.WritableCraftRegistry;
++import io.papermc.paper.registry.data.util.Conversions;
++import io.papermc.paper.registry.event.RegistryEntryAddEventImpl;
++import io.papermc.paper.registry.event.RegistryFreezeEventImpl;
+ import io.papermc.paper.registry.legacy.DelayedRegistryEntry;
+ import java.util.function.BiFunction;
+ import java.util.function.Supplier;
+@@ -11,6 +17,7 @@ import org.bukkit.Keyed;
+ import org.bukkit.NamespacedKey;
+ import org.bukkit.craftbukkit.util.ApiVersion;
+ import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.checker.nullness.qual.Nullable;
+ import org.checkerframework.framework.qual.DefaultQualifier;
+
+ @DefaultQualifier(NonNull.class)
+@@ -32,6 +39,65 @@ public interface RegistryEntry<M, B extends Keyed> extends RegistryEntryInfo<M,
+ return new DelayedRegistryEntry<>(this);
+ }
+
++ interface BuilderHolder<M, T, B extends PaperRegistryBuilder<M, T>> extends RegistryEntryInfo<M, T> {
++
++ B fillBuilder(Conversions conversions, TypedKey<T> key, M nms);
++ }
++
++ /**
++ * Can mutate values being added to the registry
++ */
++ interface Modifiable<M, T, B extends PaperRegistryBuilder<M, T>> extends BuilderHolder<M, T, B> {
++
++ static boolean isModifiable(final @Nullable RegistryEntryInfo<?, ?> entry) {
++ return entry instanceof RegistryEntry.Modifiable<?, ?, ?> || (entry instanceof final DelayedRegistryEntry<?, ?> delayed && delayed.delegate() instanceof RegistryEntry.Modifiable<?, ?, ?>);
++ }
++
++ static <M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> Modifiable<M, T, B> asModifiable(final RegistryEntryInfo<M, T> entry) { // TODO remove Keyed
++ return (Modifiable<M, T, B>) possiblyUnwrap(entry);
++ }
++
++ default RegistryEntryAddEventImpl<T, B> createEntryAddEvent(final TypedKey<T> key, final B initialBuilder, final Conversions conversions) {
++ return new RegistryEntryAddEventImpl<>(key, initialBuilder, this.apiKey(), conversions);
++ }
++ }
++
++ /**
++ * Can only add new values to the registry, not modify any values.
++ */
++ interface Addable<M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> extends BuilderHolder<M, T, B> { // TODO remove Keyed
++
++ default RegistryFreezeEventImpl<T, B> createFreezeEvent(final WritableCraftRegistry<M, T, B> writableRegistry, final Conversions conversions) {
++ return new RegistryFreezeEventImpl<>(this.apiKey(), writableRegistry.createApiWritableRegistry(conversions), conversions);
++ }
++
++ static boolean isAddable(final @Nullable RegistryEntryInfo<?, ?> entry) {
++ return entry instanceof RegistryEntry.Addable<?, ?, ?> || (entry instanceof final DelayedRegistryEntry<?, ?> delayed && delayed.delegate() instanceof RegistryEntry.Addable<?, ?, ?>);
++ }
++
++ static <M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> Addable<M, T, B> asAddable(final RegistryEntryInfo<M, T> entry) {
++ return (Addable<M, T, B>) possiblyUnwrap(entry);
++ }
++ }
++
++ /**
++ * Can mutate values and add new values.
++ */
++ interface Writable<M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> extends Modifiable<M, T, B>, Addable<M, T, B> { // TODO remove Keyed
++
++ static boolean isWritable(final @Nullable RegistryEntryInfo<?, ?> entry) {
++ return entry instanceof RegistryEntry.Writable<?, ?, ?> || (entry instanceof final DelayedRegistryEntry<?, ?> delayed && delayed.delegate() instanceof RegistryEntry.Writable<?, ?, ?>);
++ }
++
++ static <M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> Writable<M, T, B> asWritable(final RegistryEntryInfo<M, T> entry) { // TODO remove Keyed
++ return (Writable<M, T, B>) possiblyUnwrap(entry);
++ }
++ }
++
++ private static <M, B extends Keyed> RegistryEntryInfo<M, B> possiblyUnwrap(final RegistryEntryInfo<M, B> entry) {
++ return entry instanceof final DelayedRegistryEntry<M, B> delayed ? delayed.delegate() : entry;
++ }
++
+ static <M, B extends Keyed> RegistryEntry<M, B> entry(
+ final ResourceKey<? extends Registry<M>> mcKey,
+ final RegistryKey<B> apiKey,
+@@ -48,4 +114,24 @@ public interface RegistryEntry<M, B extends Keyed> extends RegistryEntryInfo<M,
+ ) {
+ return new ApiRegistryEntry<>(mcKey, apiKey, apiRegistrySupplier);
+ }
++
++ static <M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> RegistryEntry<M, T> modifiable(
++ final ResourceKey<? extends Registry<M>> mcKey,
++ final RegistryKey<T> apiKey,
++ final Class<?> toPreload,
++ final BiFunction<NamespacedKey, M, T> minecraftToBukkit,
++ final PaperRegistryBuilder.Filler<M, T, B> filler
++ ) {
++ return new ModifiableRegistryEntry<>(mcKey, apiKey, toPreload, minecraftToBukkit, filler);
++ }
++
++ static <M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> RegistryEntry<M, T> writable(
++ final ResourceKey<? extends Registry<M>> mcKey,
++ final RegistryKey<T> apiKey,
++ final Class<?> toPreload,
++ final BiFunction<NamespacedKey, M, T> minecraftToBukkit,
++ final PaperRegistryBuilder.Filler<M, T, B> filler
++ ) {
++ return new WritableRegistryEntry<>(mcKey, apiKey, toPreload, minecraftToBukkit, filler);
++ }
+ }
+diff --git a/src/main/java/io/papermc/paper/registry/entry/WritableRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/WritableRegistryEntry.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..562accce731630327d116afd1c9d559df7e386bd
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/entry/WritableRegistryEntry.java
+@@ -0,0 +1,22 @@
++package io.papermc.paper.registry.entry;
++
++import io.papermc.paper.registry.PaperRegistryBuilder;
++import io.papermc.paper.registry.RegistryKey;
++import java.util.function.BiFunction;
++import net.minecraft.core.Registry;
++import net.minecraft.resources.ResourceKey;
++import org.bukkit.Keyed;
++import org.bukkit.NamespacedKey;
++
++public class WritableRegistryEntry<M, T extends Keyed, B extends PaperRegistryBuilder<M, T>> extends AddableRegistryEntry<M, T, B> implements RegistryEntry.Writable<M, T, B> { // TODO remove Keyed
++
++ protected WritableRegistryEntry(
++ final ResourceKey<? extends Registry<M>> mcKey,
++ final RegistryKey<T> apiKey,
++ final Class<?> classToPreload,
++ final BiFunction<NamespacedKey, M, T> minecraftToBukkit,
++ final PaperRegistryBuilder.Filler<M, T, B> builderFiller
++ ) {
++ super(mcKey, apiKey, classToPreload, minecraftToBukkit, builderFiller);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEventImpl.java b/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEventImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..cc9c8fd313f530777af80ad79e03903f3f8f9829
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEventImpl.java
+@@ -0,0 +1,30 @@
++package io.papermc.paper.registry.event;
++
++import io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEvent;
++import io.papermc.paper.registry.PaperRegistries;
++import io.papermc.paper.registry.RegistryBuilder;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.TypedKey;
++import io.papermc.paper.registry.data.util.Conversions;
++import io.papermc.paper.registry.set.NamedRegistryKeySetImpl;
++import io.papermc.paper.registry.tag.Tag;
++import io.papermc.paper.registry.tag.TagKey;
++import net.minecraft.core.HolderSet;
++import net.minecraft.resources.RegistryOps;
++import org.bukkit.Keyed;
++import org.checkerframework.checker.nullness.qual.NonNull;
++
++public record RegistryEntryAddEventImpl<T, B extends RegistryBuilder<T>>(
++ TypedKey<T> key,
++ B builder,
++ RegistryKey<T> registryKey,
++ Conversions conversions
++) implements RegistryEntryAddEvent<T, B>, PaperLifecycleEvent {
++
++ @Override
++ public @NonNull <V extends Keyed> Tag<V> getOrCreateTag(final TagKey<V> tagKey) {
++ final RegistryOps.RegistryInfo<Object> registryInfo = this.conversions.lookup().lookup(PaperRegistries.registryToNms(tagKey.registryKey())).orElseThrow();
++ final HolderSet.Named<?> tagSet = registryInfo.getter().getOrThrow(PaperRegistries.toNms(tagKey));
++ return new NamedRegistryKeySetImpl<>(tagKey, tagSet);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEventMap.java b/src/main/java/io/papermc/paper/registry/event/RegistryEventMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..bfcd0884d778ca62817fb1d22f0f2ed1f2abe855
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEventMap.java
+@@ -0,0 +1,45 @@
++package io.papermc.paper.registry.event;
++
++import io.papermc.paper.plugin.bootstrap.BootstrapContext;
++import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner;
++import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType;
++import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType;
++import io.papermc.paper.registry.RegistryKey;
++import java.util.HashMap;
++import java.util.Map;
++import java.util.Objects;
++import java.util.function.BiFunction;
++
++public final class RegistryEventMap {
++
++ private final Map<RegistryKey<?>, LifecycleEventType<BootstrapContext, ? extends LifecycleEvent, ?>> eventTypes = new HashMap<>();
++ private final String name;
++
++ public RegistryEventMap(final String name) {
++ this.name = name;
++ }
++
++ @SuppressWarnings("unchecked")
++ public <T, E extends LifecycleEvent, ET extends LifecycleEventType<BootstrapContext, E, ?>> ET getOrCreate(final RegistryKey<T> registryKey, final BiFunction<? super RegistryKey<T>, ? super String, ET> eventTypeCreator) {
++ final ET eventType;
++ if (this.eventTypes.containsKey(registryKey)) {
++ eventType = (ET) this.eventTypes.get(registryKey);
++ } else {
++ eventType = eventTypeCreator.apply(registryKey, this.name);
++ this.eventTypes.put(registryKey, eventType);
++ }
++ return eventType;
++ }
++
++ @SuppressWarnings("unchecked")
++ public <T, E extends LifecycleEvent> LifecycleEventType<BootstrapContext, E, ?> getEventType(final RegistryKey<T> registryKey) {
++ return (LifecycleEventType<BootstrapContext, E, ?>) Objects.requireNonNull(this.eventTypes.get(registryKey), "No hook for " + registryKey);
++ }
++
++ public boolean hasHandlers(final RegistryKey<?> registryKey) {
++ final AbstractLifecycleEventType<?, ?, ?> type = ((AbstractLifecycleEventType<?, ?, ?>) this.eventTypes.get(registryKey));
++ return type != null && type.hasHandlers();
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProviderImpl.java b/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProviderImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..34c842ffa355e3c8001dd7b8551bcb49229a6391
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProviderImpl.java
+@@ -0,0 +1,24 @@
++package io.papermc.paper.registry.event;
++
++import io.papermc.paper.plugin.bootstrap.BootstrapContext;
++import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType;
++import io.papermc.paper.registry.PaperRegistryListenerManager;
++import io.papermc.paper.registry.RegistryBuilder;
++import io.papermc.paper.registry.event.type.RegistryEntryAddEventType;
++
++public class RegistryEventTypeProviderImpl implements RegistryEventTypeProvider {
++
++ public static RegistryEventTypeProviderImpl instance() {
++ return (RegistryEventTypeProviderImpl) RegistryEventTypeProvider.provider();
++ }
++
++ @Override
++ public <T, B extends RegistryBuilder<T>> RegistryEntryAddEventType<T, B> registryEntryAdd(final RegistryEventProvider<T, B> type) {
++ return PaperRegistryListenerManager.INSTANCE.getRegistryValueAddEventType(type);
++ }
++
++ @Override
++ public <T, B extends RegistryBuilder<T>> LifecycleEventType.Prioritizable<BootstrapContext, RegistryFreezeEvent<T, B>> registryFreeze(final RegistryEventProvider<T, B> type) {
++ return PaperRegistryListenerManager.INSTANCE.getRegistryFreezeEventType(type);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEventImpl.java b/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEventImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..63957d2509e68ccc6eb2fd9ecaa35bfad7b71b81
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEventImpl.java
+@@ -0,0 +1,28 @@
++package io.papermc.paper.registry.event;
++
++import io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEvent;
++import io.papermc.paper.registry.PaperRegistries;
++import io.papermc.paper.registry.RegistryBuilder;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.data.util.Conversions;
++import io.papermc.paper.registry.set.NamedRegistryKeySetImpl;
++import io.papermc.paper.registry.tag.Tag;
++import io.papermc.paper.registry.tag.TagKey;
++import net.minecraft.core.HolderSet;
++import net.minecraft.resources.RegistryOps;
++import org.bukkit.Keyed;
++import org.checkerframework.checker.nullness.qual.NonNull;
++
++public record RegistryFreezeEventImpl<T, B extends RegistryBuilder<T>>(
++ RegistryKey<T> registryKey,
++ WritableRegistry<T, B> registry,
++ Conversions conversions
++) implements RegistryFreezeEvent<T, B>, PaperLifecycleEvent {
++
++ @Override
++ public @NonNull <V extends Keyed> Tag<V> getOrCreateTag(final TagKey<V> tagKey) {
++ final RegistryOps.RegistryInfo<Object> registryInfo = this.conversions.lookup().lookup(PaperRegistries.registryToNms(tagKey.registryKey())).orElseThrow();
++ final HolderSet.Named<?> tagSet = registryInfo.getter().getOrThrow(PaperRegistries.toNms(tagKey));
++ return new NamedRegistryKeySetImpl<>(tagKey, tagSet);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/package-info.java b/src/main/java/io/papermc/paper/registry/event/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..14d2d9766b8dee763f220c397aba3ad432d02aaa
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/package-info.java
+@@ -0,0 +1,5 @@
++@DefaultQualifier(NonNull.class)
++package io.papermc.paper.registry.event;
++
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
+diff --git a/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventTypeImpl.java b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventTypeImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..fbf853bf1cbb3c7bbef531afe185818b9454299b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventTypeImpl.java
+@@ -0,0 +1,37 @@
++package io.papermc.paper.registry.event.type;
++
++import io.papermc.paper.plugin.bootstrap.BootstrapContext;
++import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler;
++import io.papermc.paper.plugin.lifecycle.event.types.PrioritizableLifecycleEventType;
++import io.papermc.paper.registry.RegistryBuilder;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.event.RegistryEntryAddEvent;
++import java.util.function.Consumer;
++import java.util.function.Predicate;
++
++public class RegistryEntryAddEventTypeImpl<T, B extends RegistryBuilder<T>> extends PrioritizableLifecycleEventType<BootstrapContext, RegistryEntryAddEvent<T, B>, RegistryEntryAddConfiguration<T>> implements RegistryEntryAddEventType<T, B> {
++
++ public RegistryEntryAddEventTypeImpl(final RegistryKey<T> registryKey, final String eventName) {
++ super(registryKey + " / " + eventName, BootstrapContext.class);
++ }
++
++ @Override
++ public boolean blocksReloading(final BootstrapContext eventOwner) {
++ return false; // only runs once
++ }
++
++ @Override
++ public RegistryEntryAddConfiguration<T> newHandler(final LifecycleEventHandler<? super RegistryEntryAddEvent<T, B>> handler) {
++ return new RegistryEntryAddHandlerConfiguration<>(handler, this);
++ }
++
++ @Override
++ public void forEachHandler(final RegistryEntryAddEvent<T, B> event, final Consumer<RegisteredHandler<BootstrapContext, RegistryEntryAddEvent<T, B>>> consumer, final Predicate<RegisteredHandler<BootstrapContext, RegistryEntryAddEvent<T, B>>> predicate) {
++ super.forEachHandler(event, consumer, predicate.and(handler -> this.matchesTarget(event, handler)));
++ }
++
++ private boolean matchesTarget(final RegistryEntryAddEvent<T, B> event, final RegisteredHandler<BootstrapContext, RegistryEntryAddEvent<T, B>> handler) {
++ final RegistryEntryAddHandlerConfiguration<T, B> config = (RegistryEntryAddHandlerConfiguration<T, B>) handler.config();
++ return config.filter() == null || config.filter().test(event.key());
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddHandlerConfiguration.java b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddHandlerConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..548f5bf979e88708e98d04dfe22ccaa300c91ddd
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddHandlerConfiguration.java
+@@ -0,0 +1,42 @@
++package io.papermc.paper.registry.event.type;
++
++import io.papermc.paper.plugin.bootstrap.BootstrapContext;
++import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler;
++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.PrioritizedLifecycleEventHandlerConfigurationImpl;
++import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType;
++import io.papermc.paper.registry.RegistryBuilder;
++import io.papermc.paper.registry.TypedKey;
++import io.papermc.paper.registry.event.RegistryEntryAddEvent;
++import java.util.function.Predicate;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.jetbrains.annotations.Contract;
++
++public class RegistryEntryAddHandlerConfiguration<T, B extends RegistryBuilder<T>> extends PrioritizedLifecycleEventHandlerConfigurationImpl<BootstrapContext, RegistryEntryAddEvent<T, B>> implements RegistryEntryAddConfiguration<T> {
++
++ private @Nullable Predicate<TypedKey<T>> filter;
++
++ public RegistryEntryAddHandlerConfiguration(final LifecycleEventHandler<? super RegistryEntryAddEvent<T, B>> handler, final AbstractLifecycleEventType<BootstrapContext, RegistryEntryAddEvent<T, B>, ?> eventType) {
++ super(handler, eventType);
++ }
++
++ @Contract(pure = true)
++ public @Nullable Predicate<TypedKey<T>> filter() {
++ return this.filter;
++ }
++
++ @Override
++ public RegistryEntryAddConfiguration<T> filter(final Predicate<TypedKey<T>> filter) {
++ this.filter = filter;
++ return this;
++ }
++
++ @Override
++ public RegistryEntryAddConfiguration<T> priority(final int priority) {
++ return (RegistryEntryAddConfiguration<T>) super.priority(priority);
++ }
++
++ @Override
++ public RegistryEntryAddConfiguration<T> monitor() {
++ return (RegistryEntryAddConfiguration<T>) super.monitor();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/type/RegistryLifecycleEventType.java b/src/main/java/io/papermc/paper/registry/event/type/RegistryLifecycleEventType.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7ee77022198bf5f9f88c6a1917a1da30b1863883
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryLifecycleEventType.java
+@@ -0,0 +1,18 @@
++package io.papermc.paper.registry.event.type;
++
++import io.papermc.paper.plugin.bootstrap.BootstrapContext;
++import io.papermc.paper.plugin.lifecycle.event.types.PrioritizableLifecycleEventType;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.event.RegistryEvent;
++
++public final class RegistryLifecycleEventType<T, E extends RegistryEvent<T>> extends PrioritizableLifecycleEventType.Simple<BootstrapContext, E> {
++
++ public RegistryLifecycleEventType(final RegistryKey<T> registryKey, final String eventName) {
++ super(registryKey + " / " + eventName, BootstrapContext.class);
++ }
++
++ @Override
++ public boolean blocksReloading(final BootstrapContext eventOwner) {
++ return false; // only runs once
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java b/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java
+index 9400fed345344a0a8e4fb301cca6a1867adf625b..0cdc92acd3ebb6efb10e1b66419cc05618301581 100644
+--- a/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java
++++ b/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java
+@@ -1,5 +1,7 @@
+ package io.papermc.paper.registry.legacy;
+
++import io.papermc.paper.registry.tag.Tag;
++import io.papermc.paper.registry.tag.TagKey;
+ import java.util.Iterator;
+ import java.util.function.Supplier;
+ import java.util.stream.Stream;
+@@ -7,7 +9,6 @@ import net.kyori.adventure.key.Key;
+ import org.bukkit.Keyed;
+ import org.bukkit.NamespacedKey;
+ import org.bukkit.Registry;
+-import org.bukkit.craftbukkit.CraftRegistry;
+ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+ import org.checkerframework.checker.nullness.qual.Nullable;
+ import org.jetbrains.annotations.NotNull;
+@@ -58,4 +59,14 @@ public final class DelayedRegistry<T extends Keyed, R extends Registry<T>> imple
+ public NamespacedKey getKey(final T value) {
+ return this.delegate().getKey(value);
+ }
++
++ @Override
++ public boolean hasTag(final TagKey<T> key) {
++ return this.delegate().hasTag(key);
++ }
++
++ @Override
++ public @NotNull Tag<T> getTag(final TagKey<T> key) {
++ return this.delegate().getTag(key);
++ }
+ }
+diff --git a/src/main/java/io/papermc/paper/registry/package-info.java b/src/main/java/io/papermc/paper/registry/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0b80179ff90e085568d7ceafd9b17511789dc99b
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/package-info.java
+@@ -0,0 +1,5 @@
++@DefaultQualifier(NonNull.class)
++package io.papermc.paper.registry;
++
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
+diff --git a/src/main/java/io/papermc/paper/registry/set/NamedRegistryKeySetImpl.java b/src/main/java/io/papermc/paper/registry/set/NamedRegistryKeySetImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..e8c2c18a1ed5cd587266bd415170610781531a12
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/set/NamedRegistryKeySetImpl.java
+@@ -0,0 +1,76 @@
++package io.papermc.paper.registry.set;
++
++import com.google.common.collect.ImmutableList;
++import com.google.common.collect.Iterables;
++import io.papermc.paper.registry.PaperRegistries;
++import io.papermc.paper.registry.RegistryAccess;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.TypedKey;
++import io.papermc.paper.registry.tag.Tag;
++import io.papermc.paper.registry.tag.TagKey;
++import java.util.Collection;
++import java.util.Set;
++import net.kyori.adventure.key.Key;
++import net.minecraft.core.Holder;
++import net.minecraft.core.HolderSet;
++import org.bukkit.Keyed;
++import org.bukkit.NamespacedKey;
++import org.bukkit.Registry;
++import org.bukkit.craftbukkit.util.CraftNamespacedKey;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Unmodifiable;
++
++@DefaultQualifier(NonNull.class)
++public record NamedRegistryKeySetImpl<T extends Keyed, M>( // TODO remove Keyed
++ TagKey<T> tagKey,
++ HolderSet.Named<M> namedSet
++) implements Tag<T>, org.bukkit.Tag<T> {
++
++ @Override
++ public @Unmodifiable Collection<TypedKey<T>> values() {
++ final ImmutableList.Builder<TypedKey<T>> builder = ImmutableList.builder();
++ for (final Holder<M> holder : this.namedSet) {
++ builder.add(TypedKey.create(this.tagKey.registryKey(), CraftNamespacedKey.fromMinecraft(((Holder.Reference<?>) holder).key().location())));
++ }
++ return builder.build();
++ }
++
++ @Override
++ public RegistryKey<T> registryKey() {
++ return this.tagKey.registryKey();
++ }
++
++ @Override
++ public boolean contains(final TypedKey<T> valueKey) {
++ return Iterables.any(this.namedSet, h -> {
++ return PaperRegistries.fromNms(((Holder.Reference<?>) h).key()).equals(valueKey);
++ });
++ }
++
++ @Override
++ public @Unmodifiable Collection<T> resolve(final Registry<T> registry) {
++ final ImmutableList.Builder<T> builder = ImmutableList.builder();
++ for (final Holder<M> holder : this.namedSet) {
++ builder.add(registry.getOrThrow(CraftNamespacedKey.fromMinecraft(((Holder.Reference<?>) holder).key().location())));
++ }
++ return builder.build();
++ }
++
++ @Override
++ public boolean isTagged(final T item) {
++ return this.getValues().contains(item);
++ }
++
++ @Override
++ public Set<T> getValues() {
++ return Set.copyOf(this.resolve(RegistryAccess.registryAccess().getRegistry(this.registryKey())));
++ }
++
++ @Override
++ public @NotNull NamespacedKey getKey() {
++ final Key key = this.tagKey().key();
++ return new NamespacedKey(key.namespace(), key.value());
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/set/PaperRegistrySets.java b/src/main/java/io/papermc/paper/registry/set/PaperRegistrySets.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f09ce9c8547ef05153847245746473dd9a8acbe6
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/set/PaperRegistrySets.java
+@@ -0,0 +1,48 @@
++package io.papermc.paper.registry.set;
++
++import io.papermc.paper.registry.PaperRegistries;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.TypedKey;
++import java.util.ArrayList;
++import java.util.List;
++import net.minecraft.core.Holder;
++import net.minecraft.core.HolderSet;
++import net.minecraft.core.Registry;
++import net.minecraft.resources.RegistryOps;
++import net.minecraft.resources.ResourceKey;
++import org.bukkit.Keyed;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++@DefaultQualifier(NonNull.class)
++public final class PaperRegistrySets {
++
++ public static <A extends Keyed, M> HolderSet<M> convertToNms(final ResourceKey<? extends Registry<M>> resourceKey, final RegistryOps.RegistryInfoLookup lookup, final RegistryKeySet<A> registryKeySet) { // TODO remove Keyed
++ if (registryKeySet instanceof NamedRegistryKeySetImpl<A, ?>) {
++ return ((NamedRegistryKeySetImpl<A, M>) registryKeySet).namedSet();
++ } else {
++ final RegistryOps.RegistryInfo<M> registryInfo = lookup.lookup(resourceKey).orElseThrow();
++ return HolderSet.direct(key -> {
++ return registryInfo.getter().getOrThrow(PaperRegistries.toNms(key));
++ }, registryKeySet.values());
++ }
++ }
++
++ public static <A extends Keyed, M> RegistryKeySet<A> convertToApi(final RegistryKey<A> registryKey, final HolderSet<M> holders) { // TODO remove Keyed
++ if (holders instanceof final HolderSet.Named<M> named) {
++ return new NamedRegistryKeySetImpl<>(PaperRegistries.fromNms(named.key()), named);
++ } else {
++ final List<TypedKey<A>> keys = new ArrayList<>();
++ for (final Holder<M> holder : holders) {
++ if (!(holder instanceof final Holder.Reference<M> reference)) {
++ throw new UnsupportedOperationException("Cannot convert a holder set containing direct holders");
++ }
++ keys.add(PaperRegistries.fromNms(reference.key()));
++ }
++ return RegistrySet.keySet(registryKey, keys);
++ }
++ }
++
++ private PaperRegistrySets() {
++ }
++}
+diff --git a/src/main/java/net/minecraft/core/MappedRegistry.java b/src/main/java/net/minecraft/core/MappedRegistry.java
+index 71e04e5c1bc0722abf8ca2e0738bd60b6d7ae21c..063630c1ffcce099139c59d598fc5a210e21f640 100644
+--- a/src/main/java/net/minecraft/core/MappedRegistry.java
++++ b/src/main/java/net/minecraft/core/MappedRegistry.java
+@@ -509,4 +509,12 @@ public class MappedRegistry<T> implements WritableRegistry<T> {
+
+ Stream<HolderSet.Named<T>> getTags();
+ }
++ // Paper start
++ // used to clear intrusive holders from GameEvent, Item, Block, EntityType, and Fluid from unused instances of those types
++ public void clearIntrusiveHolder(final T instance) {
++ if (this.unregisteredIntrusiveHolders != null) {
++ this.unregisteredIntrusiveHolders.remove(instance);
++ }
++ }
++ // Paper end
+ }
+diff --git a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
+index 4638ba98dbbdb0f880337347be85a6e0fbed2191..12ba8bc0a946c107b076e2c995aca6a3aeb3811f 100644
+--- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
++++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
+@@ -296,6 +296,17 @@ public class BuiltInRegistries {
+ public static final Registry<SlotDisplay.Type<?>> SLOT_DISPLAY = registerSimple(Registries.SLOT_DISPLAY, SlotDisplays::bootstrap);
+ public static final Registry<RecipeBookCategory> RECIPE_BOOK_CATEGORY = registerSimple(Registries.RECIPE_BOOK_CATEGORY, RecipeBookCategories::bootstrap);
+ public static final Registry<? extends Registry<?>> REGISTRY = WRITABLE_REGISTRY;
++ // Paper start - add built-in registry conversions
++ public static final io.papermc.paper.registry.data.util.Conversions BUILT_IN_CONVERSIONS = new io.papermc.paper.registry.data.util.Conversions(new net.minecraft.resources.RegistryOps.RegistryInfoLookup() {
++ @Override
++ public <T> java.util.Optional<net.minecraft.resources.RegistryOps.RegistryInfo<T>> lookup(final ResourceKey<? extends Registry<? extends T>> registryRef) {
++ final Registry<T> registry = net.minecraft.server.RegistryLayer.STATIC_ACCESS.registryOrThrow(registryRef);
++ return java.util.Optional.of(
++ new net.minecraft.resources.RegistryOps.RegistryInfo<>(registry.asLookup(), registry.asTagAddingLookup(), Lifecycle.experimental())
++ );
++ }
++ });
++ // Paper end - add built-in registry conversions
+
+ private static <T> Registry<T> registerSimple(ResourceKey<? extends Registry<T>> key, BuiltInRegistries.RegistryBootstrap<T> initializer) {
+ return internalRegister(key, new MappedRegistry<>(key, Lifecycle.stable(), false), initializer);
+@@ -336,6 +347,7 @@ public class BuiltInRegistries {
+ }
+ public static void bootStrap(Runnable runnable) {
+ // Paper end
++ REGISTRY.freeze(); // Paper - freeze main registry early
+ createContents();
+ runnable.run(); // Paper
+ freeze();
+@@ -355,6 +367,7 @@ public class BuiltInRegistries {
+
+ for (Registry<?> registry : REGISTRY) {
+ bindBootstrappedTagsToEmpty(registry);
++ io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.runFreezeListeners(registry.key(), BUILT_IN_CONVERSIONS); // Paper
+ registry.freeze();
+ }
+ }
+diff --git a/src/main/java/net/minecraft/resources/RegistryDataLoader.java b/src/main/java/net/minecraft/resources/RegistryDataLoader.java
+index 144a6f5b0c53110804d6d099fe857d25f107d938..f2236665eaf3e6d0f9d44605db3cd5afe0cced4e 100644
+--- a/src/main/java/net/minecraft/resources/RegistryDataLoader.java
++++ b/src/main/java/net/minecraft/resources/RegistryDataLoader.java
+@@ -130,7 +130,7 @@ public class RegistryDataLoader {
+ public static RegistryAccess.Frozen load(
+ ResourceManager resourceManager, List<HolderLookup.RegistryLookup<?>> registries, List<RegistryDataLoader.RegistryData<?>> entries
+ ) {
+- return load((loader, infoGetter) -> loader.loadFromResources(resourceManager, infoGetter), registries, entries);
++ return load((loader, infoGetter, conversions) -> loader.loadFromResources(resourceManager, infoGetter, conversions), registries, entries); // Paper - pass conversions
+ }
+
+ public static RegistryAccess.Frozen load(
+@@ -139,7 +139,7 @@ public class RegistryDataLoader {
+ List<HolderLookup.RegistryLookup<?>> registries,
+ List<RegistryDataLoader.RegistryData<?>> entries
+ ) {
+- return load((loader, infoGetter) -> loader.loadFromNetwork(data, factory, infoGetter), registries, entries);
++ return load((loader, infoGetter, conversions) -> loader.loadFromNetwork(data, factory, infoGetter, conversions), registries, entries); // Paper - pass conversions
+ }
+
+ private static RegistryAccess.Frozen load(
+@@ -148,9 +148,11 @@ public class RegistryDataLoader {
+ Map<ResourceKey<?>, Exception> map = new HashMap<>();
+ List<RegistryDataLoader.Loader<?>> list = entries.stream().map(entry -> entry.create(Lifecycle.stable(), map)).collect(Collectors.toUnmodifiableList());
+ RegistryOps.RegistryInfoLookup registryInfoLookup = createContext(registries, list);
+- list.forEach(loader -> loadable.apply((RegistryDataLoader.Loader<?>)loader, registryInfoLookup));
++ final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(registryInfoLookup); // Paper - create conversions
++ list.forEach(loader -> loadable.apply((RegistryDataLoader.Loader<?>)loader, registryInfoLookup, conversions));
+ list.forEach(loader -> {
+ Registry<?> registry = loader.registry();
++ io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.runFreezeListeners(loader.registry.key(), conversions); // Paper - run pre-freeze listeners
+
+ try {
+ registry.freeze();
+@@ -238,13 +240,13 @@ public class RegistryDataLoader {
+ }
+
+ private static <E> void loadElementFromResource(
+- WritableRegistry<E> registry, Decoder<E> decoder, RegistryOps<JsonElement> ops, ResourceKey<E> key, Resource resource, RegistrationInfo entryInfo
++ WritableRegistry<E> registry, Decoder<E> decoder, RegistryOps<JsonElement> ops, ResourceKey<E> key, Resource resource, RegistrationInfo entryInfo, io.papermc.paper.registry.data.util.Conversions conversions
+ ) throws IOException {
+ try (Reader reader = resource.openAsReader()) {
+ JsonElement jsonElement = JsonParser.parseReader(reader);
+ DataResult<E> dataResult = decoder.parse(ops, jsonElement);
+ E object = dataResult.getOrThrow();
+- registry.register(key, object, entryInfo);
++ io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerWithListeners(registry, key, object, entryInfo, conversions); // Paper - register with listeners
+ }
+ }
+
+@@ -253,7 +255,8 @@ public class RegistryDataLoader {
+ RegistryOps.RegistryInfoLookup infoGetter,
+ WritableRegistry<E> registry,
+ Decoder<E> elementDecoder,
+- Map<ResourceKey<?>, Exception> errors
++ Map<ResourceKey<?>, Exception> errors,
++ io.papermc.paper.registry.data.util.Conversions conversions // Paper - pass conversions
+ ) {
+ String string = Registries.elementsDirPath(registry.key());
+ FileToIdConverter fileToIdConverter = FileToIdConverter.json(string);
+@@ -266,7 +269,7 @@ public class RegistryDataLoader {
+ RegistrationInfo registrationInfo = REGISTRATION_INFO_CACHE.apply(resource.knownPackInfo());
+
+ try {
+- loadElementFromResource(registry, elementDecoder, registryOps, resourceKey, resource, registrationInfo);
++ loadElementFromResource(registry, elementDecoder, registryOps, resourceKey, resource, registrationInfo, conversions); // Paper - pass conversions
+ } catch (Exception var15) {
+ errors.put(
+ resourceKey,
+@@ -284,7 +287,8 @@ public class RegistryDataLoader {
+ RegistryOps.RegistryInfoLookup infoGetter,
+ WritableRegistry<E> registry,
+ Decoder<E> decoder,
+- Map<ResourceKey<?>, Exception> loadingErrors
++ Map<ResourceKey<?>, Exception> loadingErrors,
++ io.papermc.paper.registry.data.util.Conversions conversions // Paper - pass conversions
+ ) {
+ RegistryDataLoader.NetworkedRegistryData networkedRegistryData = data.get(registry.key());
+ if (networkedRegistryData != null) {
+@@ -311,7 +315,7 @@ public class RegistryDataLoader {
+
+ try {
+ Resource resource = factory.getResourceOrThrow(resourceLocation);
+- loadElementFromResource(registry, decoder, registryOps2, resourceKey, resource, NETWORK_REGISTRATION_INFO);
++ loadElementFromResource(registry, decoder, registryOps2, resourceKey, resource, NETWORK_REGISTRATION_INFO, conversions); // Paper - pass conversions
+ } catch (Exception var18) {
+ loadingErrors.put(resourceKey, new IllegalStateException("Failed to parse local data", var18));
+ }
+@@ -323,22 +327,23 @@ public class RegistryDataLoader {
+ }
+
+ static record Loader<T>(RegistryDataLoader.RegistryData<T> data, WritableRegistry<T> registry, Map<ResourceKey<?>, Exception> loadingErrors) {
+- public void loadFromResources(ResourceManager resourceManager, RegistryOps.RegistryInfoLookup infoGetter) {
+- RegistryDataLoader.loadContentsFromManager(resourceManager, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors);
++ public void loadFromResources(ResourceManager resourceManager, RegistryOps.RegistryInfoLookup infoGetter, io.papermc.paper.registry.data.util.Conversions conversions) { // Paper - pass conversions
++ RegistryDataLoader.loadContentsFromManager(resourceManager, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors, conversions); // Paper - pass conversions
+ }
+
+ public void loadFromNetwork(
+ Map<ResourceKey<? extends Registry<?>>, RegistryDataLoader.NetworkedRegistryData> data,
+ ResourceProvider factory,
+- RegistryOps.RegistryInfoLookup infoGetter
++ RegistryOps.RegistryInfoLookup infoGetter,
++ io.papermc.paper.registry.data.util.Conversions conversions // Paper
+ ) {
+- RegistryDataLoader.loadContentsFromNetwork(data, factory, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors);
++ RegistryDataLoader.loadContentsFromNetwork(data, factory, infoGetter, this.registry, this.data.elementCodec, this.loadingErrors, conversions); // Paper - pass conversions
+ }
+ }
+
+ @FunctionalInterface
+ interface LoadingFunction {
+- void apply(RegistryDataLoader.Loader<?> loader, RegistryOps.RegistryInfoLookup infoGetter);
++ void apply(RegistryDataLoader.Loader<?> loader, RegistryOps.RegistryInfoLookup infoGetter, io.papermc.paper.registry.data.util.Conversions conversions); // Paper - pass conversions
+ }
+
+ public static record NetworkedRegistryData(List<RegistrySynchronization.PackedRegistryEntry> elements, TagNetworkSerialization.NetworkPayload tags) {
+diff --git a/src/main/java/net/minecraft/server/ReloadableServerRegistries.java b/src/main/java/net/minecraft/server/ReloadableServerRegistries.java
+index 6fddef967b6314ca0158f5bd4b8898670ea5e9ec..b5ca1a0acb16d0cd8dccc854f309d425a48b070d 100644
+--- a/src/main/java/net/minecraft/server/ReloadableServerRegistries.java
++++ b/src/main/java/net/minecraft/server/ReloadableServerRegistries.java
+@@ -50,8 +50,9 @@ public class ReloadableServerRegistries {
+ );
+ HolderLookup.Provider provider = HolderLookup.Provider.create(list.stream());
+ RegistryOps<JsonElement> registryOps = provider.createSerializationContext(JsonOps.INSTANCE);
++ final io.papermc.paper.registry.data.util.Conversions conversions = new io.papermc.paper.registry.data.util.Conversions(registryOps.lookupProvider); // Paper
+ List<CompletableFuture<WritableRegistry<?>>> list2 = LootDataType.values()
+- .map(type -> scheduleRegistryLoad((LootDataType<?>)type, registryOps, resourceManager, prepareExecutor))
++ .map(type -> scheduleRegistryLoad((LootDataType<?>)type, registryOps, resourceManager, prepareExecutor, conversions)) // Paper
+ .toList();
+ CompletableFuture<List<WritableRegistry<?>>> completableFuture = Util.sequence(list2);
+ return completableFuture.thenApplyAsync(
+@@ -60,7 +61,7 @@ public class ReloadableServerRegistries {
+ }
+
+ private static <T> CompletableFuture<WritableRegistry<?>> scheduleRegistryLoad(
+- LootDataType<T> type, RegistryOps<JsonElement> ops, ResourceManager resourceManager, Executor prepareExecutor
++ LootDataType<T> type, RegistryOps<JsonElement> ops, ResourceManager resourceManager, Executor prepareExecutor, io.papermc.paper.registry.data.util.Conversions conversions // Paper
+ ) {
+ return CompletableFuture.supplyAsync(() -> {
+ WritableRegistry<T> writableRegistry = new MappedRegistry<>(type.registryKey(), Lifecycle.experimental());
+@@ -68,7 +69,7 @@ public class ReloadableServerRegistries {
+ Map<ResourceLocation, T> map = new HashMap<>();
+ String string = Registries.elementsDirPath(type.registryKey());
+ SimpleJsonResourceReloadListener.scanDirectory(resourceManager, string, ops, type.codec(), map);
+- map.forEach((id, value) -> writableRegistry.register(ResourceKey.create(type.registryKey(), id), (T)value, DEFAULT_REGISTRATION_INFO));
++ map.forEach((id, value) -> io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerWithListeners(writableRegistry, ResourceKey.create(type.registryKey(), id), value, DEFAULT_REGISTRATION_INFO, conversions)); // Paper - register with listeners
+ TagLoader.loadTagsForRegistry(resourceManager, writableRegistry);
+ return writableRegistry;
+ }, prepareExecutor);
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java
+index f8450a2abd1e96fac7827d252cc00038b9dee839..a812a42ea81b1543287e78ea55da6cbf4e0d27f8 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java
+@@ -167,11 +167,11 @@ public class CraftRegistry<B extends Keyed, M> implements Registry<B> {
+ private final Map<NamespacedKey, B> cache = new HashMap<>();
+ private final Map<B, NamespacedKey> byValue = new java.util.IdentityHashMap<>(); // Paper - improve Registry
+ private final net.minecraft.core.Registry<M> minecraftRegistry;
+- private final BiFunction<NamespacedKey, M, B> minecraftToBukkit;
++ private final BiFunction<? super NamespacedKey, M, B> minecraftToBukkit; // Paper
+ private final BiFunction<NamespacedKey, ApiVersion, NamespacedKey> serializationUpdater; // Paper - rename to make it *clear* what it is *only* for
+ private boolean init;
+
+- public CraftRegistry(Class<?> bukkitClass, net.minecraft.core.Registry<M> minecraftRegistry, BiFunction<NamespacedKey, M, B> minecraftToBukkit, BiFunction<NamespacedKey, ApiVersion, NamespacedKey> serializationUpdater) { // Paper - relax preload class
++ public CraftRegistry(Class<?> bukkitClass, net.minecraft.core.Registry<M> minecraftRegistry, BiFunction<? super NamespacedKey, M, B> minecraftToBukkit, BiFunction<NamespacedKey, ApiVersion, NamespacedKey> serializationUpdater) { // Paper - relax preload class
+ this.bukkitClass = bukkitClass;
+ this.minecraftRegistry = minecraftRegistry;
+ this.minecraftToBukkit = minecraftToBukkit;
+@@ -254,4 +254,17 @@ public class CraftRegistry<B extends Keyed, M> implements Registry<B> {
+ return this.byValue.get(value);
+ }
+ // Paper end - improve Registry
++
++ // Paper start - RegistrySet API
++ @Override
++ public boolean hasTag(final io.papermc.paper.registry.tag.TagKey<B> key) {
++ return this.minecraftRegistry.getTag(net.minecraft.tags.TagKey.create(this.minecraftRegistry.key(), io.papermc.paper.adventure.PaperAdventure.asVanilla(key.key()))).isPresent();
++ }
++
++ @Override
++ public io.papermc.paper.registry.tag.Tag<B> getTag(final io.papermc.paper.registry.tag.TagKey<B> key) {
++ final net.minecraft.core.HolderSet.Named<M> namedHolderSet = this.minecraftRegistry.getTag(io.papermc.paper.registry.PaperRegistries.toNms(key)).orElseThrow();
++ return new io.papermc.paper.registry.set.NamedRegistryKeySetImpl<>(key, namedHolderSet);
++ }
++ // Paper end - RegistrySet API
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+index b24ccbff89db873f5bdf62cbebcca0049b94a8d5..49b898ed5e9de2507a6a6aac61dea4fe902649ca 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -674,6 +674,21 @@ public final class CraftMagicNumbers implements UnsafeValues {
+ }
+ // Paper end - lifecycle event API
+
++ // Paper start - hack to get tags for non server-backed registries
++ @Override
++ public <A extends Keyed, M> io.papermc.paper.registry.tag.Tag<A> getTag(final io.papermc.paper.registry.tag.TagKey<A> tagKey) { // TODO remove Keyed
++ if (tagKey.registryKey() != io.papermc.paper.registry.RegistryKey.ENTITY_TYPE && tagKey.registryKey() != io.papermc.paper.registry.RegistryKey.FLUID) {
++ throw new UnsupportedOperationException(tagKey.registryKey() + " doesn't have tags");
++ }
++ final net.minecraft.resources.ResourceKey<? extends net.minecraft.core.Registry<M>> nmsKey = io.papermc.paper.registry.PaperRegistries.registryToNms(tagKey.registryKey());
++ final net.minecraft.core.Registry<M> nmsRegistry = org.bukkit.craftbukkit.CraftRegistry.getMinecraftRegistry().registryOrThrow(nmsKey);
++ return nmsRegistry
++ .getTag(io.papermc.paper.registry.PaperRegistries.toNms(tagKey))
++ .map(named -> new io.papermc.paper.registry.set.NamedRegistryKeySetImpl<>(tagKey, named))
++ .orElse(null);
++ }
++ // Paper end - hack to get tags for non server-backed registries
++
+ /**
+ * This helper class represents the different NBT Tags.
+ * <p>
+diff --git a/src/main/resources/META-INF/services/io.papermc.paper.registry.event.RegistryEventTypeProvider b/src/main/resources/META-INF/services/io.papermc.paper.registry.event.RegistryEventTypeProvider
+new file mode 100644
+index 0000000000000000000000000000000000000000..8bee1a5ed877a04e4d027593df1f42cefdd824e7
+--- /dev/null
++++ b/src/main/resources/META-INF/services/io.papermc.paper.registry.event.RegistryEventTypeProvider
+@@ -0,0 +1 @@
++io.papermc.paper.registry.event.RegistryEventTypeProviderImpl
+diff --git a/src/test/java/io/papermc/paper/registry/RegistryBuilderTest.java b/src/test/java/io/papermc/paper/registry/RegistryBuilderTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..47b8ebac8496179008b8932c5ca2aadc274e24e0
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/registry/RegistryBuilderTest.java
+@@ -0,0 +1,36 @@
++package io.papermc.paper.registry;
++
++import io.papermc.paper.registry.data.util.Conversions;
++import java.util.List;
++import java.util.Map;
++import net.minecraft.core.Registry;
++import net.minecraft.resources.RegistryOps;
++import net.minecraft.resources.ResourceKey;
++import org.bukkit.support.RegistryHelper;
++import org.bukkit.support.environment.AllFeatures;
++import org.junit.jupiter.api.Disabled;
++import org.junit.jupiter.params.ParameterizedTest;
++import org.junit.jupiter.params.provider.Arguments;
++import org.junit.jupiter.params.provider.MethodSource;
++
++import static org.junit.jupiter.api.Assertions.assertEquals;
++
++@AllFeatures
++class RegistryBuilderTest {
++
++ static List<Arguments> registries() {
++ return List.of(
++ );
++ }
++
++ @Disabled
++ @ParameterizedTest
++ @MethodSource("registries")
++ <M, T> void testEquality(final ResourceKey<? extends Registry<M>> resourceKey, final PaperRegistryBuilder.Filler<M, T, ?> filler) {
++ final Registry<M> registry = RegistryHelper.getRegistry().registryOrThrow(resourceKey);
++ for (final Map.Entry<ResourceKey<M>, M> entry : registry.entrySet()) {
++ final M built = filler.fill(new Conversions(new RegistryOps.HolderLookupAdapter(RegistryHelper.getRegistry())), PaperRegistries.fromNms(entry.getKey()), entry.getValue()).build();
++ assertEquals(entry.getValue(), built);
++ }
++ }
++}
+diff --git a/src/test/java/org/bukkit/registry/RegistryClassTest.java b/src/test/java/org/bukkit/registry/RegistryClassTest.java
+index ea3d37f387bdb0dd5ae3fba9231ace31d0cebd64..c118c911972fe5a9f0c3e306009306f04ae2e821 100644
+--- a/src/test/java/org/bukkit/registry/RegistryClassTest.java
++++ b/src/test/java/org/bukkit/registry/RegistryClassTest.java
+@@ -111,7 +111,7 @@ public class RegistryClassTest {
+ outsideRequest.clear();
+ MockUtil.resetMock(spyRegistry);
+ doAnswer(invocation -> {
+- Keyed item = realRegistry.get(invocation.getArgument(0));
++ Keyed item = realRegistry.get((NamespacedKey) invocation.getArgument(0)); // Paper - registry modification api - specifically call namespaced key overload
+
+ if (item == null) {
+ nullValue.add(invocation.getArgument(0));
+@@ -120,10 +120,10 @@ public class RegistryClassTest {
+ nullAble.add(invocation.getArgument(0));
+
+ return item;
+- }).when(spyRegistry).get(any());
++ }).when(spyRegistry).get((NamespacedKey) any()); // Paper - registry modification api - specifically call namespaced key overload
+
+ doAnswer(invocation -> {
+- Keyed item = realRegistry.get(invocation.getArgument(0));
++ Keyed item = realRegistry.get((NamespacedKey) invocation.getArgument(0)); // Paper - registry modification api - specifically call namespaced key overload
+
+ if (item == null) {
+ nullValue.add(invocation.getArgument(0));
+@@ -138,7 +138,7 @@ public class RegistryClassTest {
+ notNullAble.add(invocation.getArgument(0));
+
+ return item;
+- }).when(spyRegistry).getOrThrow(any());
++ }).when(spyRegistry).getOrThrow((NamespacedKey) any()); // Paper - registry modification api - specifically call namespaced key overload
+
+ // Load class
+ try {
+@@ -171,13 +171,13 @@ public class RegistryClassTest {
+ outsideRequest
+ .computeIfAbsent(type, ty -> new ArrayList<>()).add(invocation.getArgument(0));
+ return mock(type);
+- }).when(spyRegistry).get(any());
++ }).when(spyRegistry).get((NamespacedKey) any()); // Paper - registry modification api - specifically call namespaced key overload
+
+ doAnswer(invocation -> {
+ outsideRequest
+ .computeIfAbsent(type, ty -> new ArrayList<>()).add(invocation.getArgument(0));
+ return mock(type);
+- }).when(spyRegistry).getOrThrow(any());
++ }).when(spyRegistry).getOrThrow((NamespacedKey) any()); // Paper - registry modification api - specifically call namespaced key overload
+ }
+
+ private static void initFieldDataCache() {
+diff --git a/src/test/java/org/bukkit/support/extension/NormalExtension.java b/src/test/java/org/bukkit/support/extension/NormalExtension.java
+index 8b5dcc4d0ecf7f9c51f73080c123ca08e31b1898..a809ea2f0d2b477c61857aa02a7e55024b2a7e0d 100644
+--- a/src/test/java/org/bukkit/support/extension/NormalExtension.java
++++ b/src/test/java/org/bukkit/support/extension/NormalExtension.java
+@@ -62,7 +62,7 @@ public class NormalExtension extends BaseExtension {
+
+ doAnswer(invocation ->
+ mocks.computeIfAbsent(invocation.getArgument(0), k -> mock(RegistryHelper.updateClass(keyed, invocation.getArgument(0)), withSettings().stubOnly().defaultAnswer(DEFAULT_ANSWER)))
+- ).when(registry).get(any()); // Allow static classes to fill there fields, so that it does not error out, just by loading them
++ ).when(registry).get((NamespacedKey) any()); // Allow static classes to fill there fields, so that it does not error out, just by loading them // Paper - registry modification api - specifically call namespaced key overload
+
+ return registry;
+ }