aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJake Potrebic <[email protected]>2024-06-16 12:39:59 -0700
committerGitHub <[email protected]>2024-06-16 12:39:59 -0700
commitd9111ccec278132052cebdebca9f1c6dcaba9512 (patch)
tree371066574c947f764742e741e5a0238f69d49305
parent106dbae220686b2a01abf570a7925757940f68a3 (diff)
downloadPaper-d9111ccec278132052cebdebca9f1c6dcaba9512.tar.gz
Paper-d9111ccec278132052cebdebca9f1c6dcaba9512.zip
Registry Modification API (#10893)
* Registry Modification API * some fixes * even more fixes
-rw-r--r--.editorconfig2
-rw-r--r--patches/api/0003-Test-changes.patch24
-rw-r--r--patches/api/0073-AsyncTabCompleteEvent.patch2
-rw-r--r--patches/api/0433-Improve-Registry.patch36
-rw-r--r--patches/api/0476-Registry-Modification-API.patch893
-rw-r--r--patches/api/0476-WIP-Tag-API.patch62
-rw-r--r--patches/server/0010-Adventure.patch26
-rw-r--r--patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch6
-rw-r--r--patches/server/0019-Paper-Plugins.patch13
-rw-r--r--patches/server/0237-Optimize-MappedRegistry.patch2
-rw-r--r--patches/server/0475-Add-RegistryAccess-for-managing-Registries.patch97
-rw-r--r--patches/server/0934-Add-Lifecycle-Event-system.patch156
-rw-r--r--patches/server/0975-Brigadier-based-command-API.patch8
-rw-r--r--patches/server/1021-Registry-Modification-API.patch1427
14 files changed, 2547 insertions, 207 deletions
diff --git a/.editorconfig b/.editorconfig
index 2874476cf4..198db6e8a1 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -29,6 +29,8 @@ ij_java_names_count_to_use_import_on_demand = 999999
ij_java_imports_layout = *,|,$*
ij_java_generate_final_locals = true
ij_java_generate_final_parameters = true
+ij_java_method_parameters_new_line_after_left_paren = true
+ij_java_method_parameters_right_paren_on_new_line = true
[test-plugin/**/*.java]
ij_java_use_fq_class_names = false
diff --git a/patches/api/0003-Test-changes.patch b/patches/api/0003-Test-changes.patch
index a9fa8a4131..a1db2f75de 100644
--- a/patches/api/0003-Test-changes.patch
+++ b/patches/api/0003-Test-changes.patch
@@ -66,7 +66,7 @@ index 0000000000000000000000000000000000000000..77154095cfb8b259bdb318e8ff40cb6f
+ }
+}
diff --git a/src/test/java/org/bukkit/AnnotationTest.java b/src/test/java/org/bukkit/AnnotationTest.java
-index 64e7aef6220097edefdff3b98a771b988365930d..a899f63eb2ce58b3cf708e91819cbbdeffda5d9f 100644
+index 64e7aef6220097edefdff3b98a771b988365930d..d9091ba1e5a55e03adca98305233cce9d6888609 100644
--- a/src/test/java/org/bukkit/AnnotationTest.java
+++ b/src/test/java/org/bukkit/AnnotationTest.java
@@ -29,7 +29,13 @@ public class AnnotationTest {
@@ -103,7 +103,7 @@ index 64e7aef6220097edefdff3b98a771b988365930d..a899f63eb2ce58b3cf708e91819cbbde
};
@Test
-@@ -67,14 +83,40 @@ public class AnnotationTest {
+@@ -67,14 +83,48 @@ public class AnnotationTest {
}
if (mustBeAnnotated(Type.getReturnType(method.desc)) && !isWellAnnotated(method.invisibleAnnotations)) {
@@ -132,8 +132,16 @@ index 64e7aef6220097edefdff3b98a771b988365930d..a899f63eb2ce58b3cf708e91819cbbde
for (int i = 0; i < paramTypes.length; i++) {
if (mustBeAnnotated(paramTypes[i]) ^ isWellAnnotated(method.invisibleParameterAnnotations == null ? null : method.invisibleParameterAnnotations[i])) {
+ // Paper start
-+ if (method.invisibleTypeAnnotations != null || method.visibleTypeAnnotations != null) {
-+ for (final org.objectweb.asm.tree.TypeAnnotationNode invisibleTypeAnnotation : java.util.Objects.requireNonNullElse(method.invisibleTypeAnnotations, method.visibleTypeAnnotations)) {
++ if (method.invisibleTypeAnnotations != null) {
++ for (final org.objectweb.asm.tree.TypeAnnotationNode invisibleTypeAnnotation : method.invisibleTypeAnnotations) {
++ final org.objectweb.asm.TypeReference ref = new org.objectweb.asm.TypeReference(invisibleTypeAnnotation.typeRef);
++ if (ref.getSort() == org.objectweb.asm.TypeReference.METHOD_FORMAL_PARAMETER && ref.getTypeParameterIndex() == i && java.util.Arrays.asList(ACCEPTED_ANNOTATIONS).contains(invisibleTypeAnnotation.desc)) {
++ continue dancing;
++ }
++ }
++ }
++ if (method.visibleTypeAnnotations != null) {
++ for (final org.objectweb.asm.tree.TypeAnnotationNode invisibleTypeAnnotation : method.visibleTypeAnnotations) {
+ final org.objectweb.asm.TypeReference ref = new org.objectweb.asm.TypeReference(invisibleTypeAnnotation.typeRef);
+ if (ref.getSort() == org.objectweb.asm.TypeReference.METHOD_FORMAL_PARAMETER && ref.getTypeParameterIndex() == i && java.util.Arrays.asList(ACCEPTED_ANNOTATIONS).contains(invisibleTypeAnnotation.desc)) {
+ continue dancing;
@@ -144,7 +152,7 @@ index 64e7aef6220097edefdff3b98a771b988365930d..a899f63eb2ce58b3cf708e91819cbbde
ParameterNode paramNode = parameters == null ? null : parameters.get(i);
String paramName = paramNode == null ? null : paramNode.name;
-@@ -91,13 +133,18 @@ public class AnnotationTest {
+@@ -91,13 +141,18 @@ public class AnnotationTest {
Collections.sort(errors);
@@ -167,7 +175,7 @@ index 64e7aef6220097edefdff3b98a771b988365930d..a899f63eb2ce58b3cf708e91819cbbde
}
private static void collectClasses(@NotNull File from, @NotNull Map<String, ClassNode> to) throws IOException {
-@@ -140,6 +187,11 @@ public class AnnotationTest {
+@@ -140,6 +195,11 @@ public class AnnotationTest {
// Exceptions are excluded
return false;
}
@@ -179,7 +187,7 @@ index 64e7aef6220097edefdff3b98a771b988365930d..a899f63eb2ce58b3cf708e91819cbbde
for (String excludedClass : EXCLUDED_CLASSES) {
if (excludedClass.equals(clazz.name)) {
-@@ -152,7 +204,7 @@ public class AnnotationTest {
+@@ -152,7 +212,7 @@ public class AnnotationTest {
private static boolean isMethodIncluded(@NotNull ClassNode clazz, @NotNull MethodNode method, @NotNull Map<String, ClassNode> allClasses) {
// Exclude private, synthetic and deprecated methods
@@ -188,7 +196,7 @@ index 64e7aef6220097edefdff3b98a771b988365930d..a899f63eb2ce58b3cf708e91819cbbde
return false;
}
-@@ -170,11 +222,30 @@ public class AnnotationTest {
+@@ -170,11 +230,30 @@ public class AnnotationTest {
if ("<init>".equals(method.name) && isAnonymous(clazz)) {
return false;
}
diff --git a/patches/api/0073-AsyncTabCompleteEvent.patch b/patches/api/0073-AsyncTabCompleteEvent.patch
index b88930e4dc..3908b1a718 100644
--- a/patches/api/0073-AsyncTabCompleteEvent.patch
+++ b/patches/api/0073-AsyncTabCompleteEvent.patch
@@ -589,7 +589,7 @@ index 270e6d8ad4358baa256cee5f16cff281f063ce3b..b43c3cb5c88eada186d6f81712c244aa
@Override
diff --git a/src/test/java/org/bukkit/AnnotationTest.java b/src/test/java/org/bukkit/AnnotationTest.java
-index a899f63eb2ce58b3cf708e91819cbbdeffda5d9f..057dc3ebea3516863dda24252fe05d344c16fab3 100644
+index d9091ba1e5a55e03adca98305233cce9d6888609..b82f07a2879412f6b30643ca93a97439aa49a98a 100644
--- a/src/test/java/org/bukkit/AnnotationTest.java
+++ b/src/test/java/org/bukkit/AnnotationTest.java
@@ -48,6 +48,8 @@ public class AnnotationTest {
diff --git a/patches/api/0433-Improve-Registry.patch b/patches/api/0433-Improve-Registry.patch
index 8a49d39633..d5cb134c45 100644
--- a/patches/api/0433-Improve-Registry.patch
+++ b/patches/api/0433-Improve-Registry.patch
@@ -31,15 +31,45 @@ index 62d2b3f950860dee0898d77b0a29635c3f9a7e23..704dba92f9246ef398ed8d162ebee3cf
@Override
public @NotNull String translationKey() {
diff --git a/src/main/java/org/bukkit/Registry.java b/src/main/java/org/bukkit/Registry.java
-index 17714f04fdd87ed4332ea62bcfab7063560bf1be..bc7144f02ac6857dbeec13c3995b71f6920e022a 100644
+index 17714f04fdd87ed4332ea62bcfab7063560bf1be..27b987db385a594fede4e884b6437dc363f6e817 100644
--- a/src/main/java/org/bukkit/Registry.java
+++ b/src/main/java/org/bukkit/Registry.java
-@@ -358,6 +358,49 @@ public interface Registry<T extends Keyed> extends Iterable<T> {
+@@ -358,6 +358,79 @@ public interface Registry<T extends Keyed> extends Iterable<T> {
@Nullable
T get(@NotNull NamespacedKey key);
+ // Paper start - improve Registry
+ /**
++ * Gets the object by its key or throws if it doesn't exist.
++ *
++ * @param key the key to get the object of in this registry
++ * @return the object for the key
++ * @throws java.util.NoSuchElementException if the key doesn't point to an object in the registry
++ */
++ default @NotNull T getOrThrow(final net.kyori.adventure.key.@NotNull Key key) {
++ final T value = this.get(key);
++ if (value == null) {
++ throw new java.util.NoSuchElementException("No value for " + key + " in " + this);
++ }
++ return value;
++ }
++
++ /**
++ * Gets the object by its key or throws if it doesn't exist.
++ *
++ * @param key the key to get the object of in this registry
++ * @return the object for the key
++ * @throws java.util.NoSuchElementException if the key doesn't point to an object in the registry
++ */
++ default @NotNull T getOrThrow(final io.papermc.paper.registry.@NotNull TypedKey<T> key) {
++ final T value = this.get(key);
++ if (value == null) {
++ throw new java.util.NoSuchElementException("No value for " + key + " in " + this);
++ }
++ return value;
++ }
++
++ /**
+ * Gets the key for this object or throws if it doesn't exist.
+ * <p>
+ * Some types can exist without being in a registry
@@ -84,7 +114,7 @@ index 17714f04fdd87ed4332ea62bcfab7063560bf1be..bc7144f02ac6857dbeec13c3995b71f6
/**
* Returns a new stream, which contains all registry items, which are registered to the registry.
*
-@@ -432,5 +475,12 @@ public interface Registry<T extends Keyed> extends Iterable<T> {
+@@ -432,5 +505,12 @@ public interface Registry<T extends Keyed> extends Iterable<T> {
public Class<T> getType() {
return this.type;
}
diff --git a/patches/api/0476-Registry-Modification-API.patch b/patches/api/0476-Registry-Modification-API.patch
new file mode 100644
index 0000000000..f566c2d39d
--- /dev/null
+++ b/patches/api/0476-Registry-Modification-API.patch
@@ -0,0 +1,893 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <[email protected]>
+Date: Wed, 2 Mar 2022 13:36:21 -0800
+Subject: [PATCH] Registry Modification API
+
+
+diff --git a/src/main/java/io/papermc/paper/registry/RegistryBuilder.java b/src/main/java/io/papermc/paper/registry/RegistryBuilder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6edf300c1d81c1001756141c9efd022ba0e372cd
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/RegistryBuilder.java
+@@ -0,0 +1,13 @@
++package io.papermc.paper.registry;
++
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * To be implemented by any type used for modifying registries.
++ *
++ * @param <T> registry value type
++ */
++public interface RegistryBuilder<T> {
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEvent.java b/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a5d7385eae9dfb88b52aed0e42c09a10ef807385
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEntryAddEvent.java
+@@ -0,0 +1,47 @@
++package io.papermc.paper.registry.event;
++
++import io.papermc.paper.registry.RegistryBuilder;
++import io.papermc.paper.registry.TypedKey;
++import io.papermc.paper.registry.tag.Tag;
++import io.papermc.paper.registry.tag.TagKey;
++import org.bukkit.Keyed;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * Event object for {@link RegistryEventProvider#entryAdd()}. This
++ * event is fired right before a specific entry is registered in/added to registry.
++ * It provides a way for plugins to modify parts of this entry.
++ *
++ * @param <T> registry entry type
++ * @param <B> registry entry builder type
++ */
++public interface RegistryEntryAddEvent<T, B extends RegistryBuilder<T>> extends RegistryEvent<T> {
++
++ /**
++ * Gets the builder for the entry being added to the registry.
++ *
++ * @return the object builder
++ */
++ @NonNull B builder();
++
++ /**
++ * Gets the key for this entry in the registry.
++ *
++ * @return the key
++ */
++ @NonNull TypedKey<T> key();
++
++ /**
++ * Gets or creates a tag for the given tag key. This tag
++ * is then required to be filled either from the built-in or
++ * custom datapack.
++ *
++ * @param tagKey the tag key
++ * @return the tag
++ * @param <V> the tag value type
++ */
++ @NonNull <V extends Keyed> Tag<V> getOrCreateTag(@NonNull TagKey<V> tagKey);
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEvent.java b/src/main/java/io/papermc/paper/registry/event/RegistryEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..498c5920926158e86c2aec2bd2129d5e8b8613a3
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEvent.java
+@@ -0,0 +1,23 @@
++package io.papermc.paper.registry.event;
++
++import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
++import io.papermc.paper.registry.RegistryKey;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * Base type for all registry events.
++ *
++ * @param <T> registry entry type
++ */
++public interface RegistryEvent<T> extends LifecycleEvent {
++
++ /**
++ * Get the key for the registry this event pertains to.
++ *
++ * @return the registry key
++ */
++ @NonNull RegistryKey<T> registryKey();
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEventProvider.java b/src/main/java/io/papermc/paper/registry/event/RegistryEventProvider.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..477ed0fd5acc923d429980529876f0dd7dd3a52a
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEventProvider.java
+@@ -0,0 +1,58 @@
++package io.papermc.paper.registry.event;
++
++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.LifecycleEventHandlerConfiguration;
++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.PrioritizedLifecycleEventHandlerConfiguration;
++import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType;
++import io.papermc.paper.registry.RegistryBuilder;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.event.type.RegistryEntryAddEventType;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * Provider for registry events for a specific registry.
++ * <p>
++ * Supported events are:
++ * <ul>
++ * <li>{@link RegistryEntryAddEvent} (via {@link #entryAdd()})</li>
++ * <li>{@link RegistryFreezeEvent} (via {@link #freeze()})</li>
++ * </ul>
++ *
++ * @param <T> registry entry type
++ * @param <B> registry entry builder type
++ */
++public interface RegistryEventProvider<T, B extends RegistryBuilder<T>> {
++
++ /**
++ * Gets the event type for {@link RegistryEntryAddEvent} which is fired just before
++ * an object is added to a registry.
++ * <p>
++ * Can be used in {@link io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager#registerEventHandler(LifecycleEventType, LifecycleEventHandler)}
++ * to register a handler for {@link RegistryEntryAddEvent}.
++ *
++ * @return the registry entry add event type
++ */
++ @NonNull RegistryEntryAddEventType<T, B> entryAdd();
++
++ /**
++ * Gets the event type for {@link RegistryFreezeEvent} which is fired just before
++ * a registry is frozen. It allows for the registration of new objects.
++ * <p>
++ * Can be used in {@link io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager#registerEventHandler(LifecycleEventType, LifecycleEventHandler)}
++ * to register a handler for {@link RegistryFreezeEvent}.
++ *
++ * @return the registry freeze event type
++ */
++ LifecycleEventType.@NonNull Prioritizable<BootstrapContext, RegistryFreezeEvent<T, B>> freeze();
++
++ /**
++ * Gets the registry key associated with this event type provider.
++ *
++ * @return the registry key
++ */
++ @NonNull RegistryKey<T> registryKey();
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEventProviderImpl.java b/src/main/java/io/papermc/paper/registry/event/RegistryEventProviderImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..cfe47c8bd0888db6d00867acfefc8db42ef314aa
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEventProviderImpl.java
+@@ -0,0 +1,30 @@
++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.RegistryBuilder;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.event.type.RegistryEntryAddEventType;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.jetbrains.annotations.ApiStatus;
++
++@DefaultQualifier(NonNull.class)
++record RegistryEventProviderImpl<T, B extends RegistryBuilder<T>>(RegistryKey<T> registryKey) implements RegistryEventProvider<T, B> {
++
++ static <T, B extends RegistryBuilder<T>> RegistryEventProvider<T, B> create(final RegistryKey<T> registryKey) {
++ return new RegistryEventProviderImpl<>(registryKey);
++ }
++
++ @Override
++ public RegistryEntryAddEventType<T, B> entryAdd() {
++ return RegistryEventTypeProvider.provider().registryEntryAdd(this);
++ }
++
++ @Override
++ public LifecycleEventType.Prioritizable<BootstrapContext, RegistryFreezeEvent<T, B>> freeze() {
++ return RegistryEventTypeProvider.provider().registryFreeze(this);
++ }
++
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProvider.java b/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProvider.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d807bd2f42c98e37a96cf110ad77820dfffc8398
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEventTypeProvider.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.RegistryBuilder;
++import io.papermc.paper.registry.event.type.RegistryEntryAddEventType;
++import java.util.Optional;
++import java.util.ServiceLoader;
++import org.jetbrains.annotations.ApiStatus;
++
++interface RegistryEventTypeProvider {
++
++ Optional<RegistryEventTypeProvider> PROVIDER = ServiceLoader.load(RegistryEventTypeProvider.class)
++ .findFirst();
++
++ static RegistryEventTypeProvider provider() {
++ return PROVIDER.orElseThrow(() -> new IllegalStateException("Could not find a %s service implementation".formatted(RegistryEventTypeProvider.class.getSimpleName())));
++ }
++
++ <T, B extends RegistryBuilder<T>> RegistryEntryAddEventType<T, B> registryEntryAdd(RegistryEventProvider<T, B> type);
++
++ <T, B extends RegistryBuilder<T>> LifecycleEventType.Prioritizable<BootstrapContext, RegistryFreezeEvent<T, B>> registryFreeze(RegistryEventProvider<T, B> type);
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryEvents.java b/src/main/java/io/papermc/paper/registry/event/RegistryEvents.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1f89945be2ed68f52a544f41f7a151b8fdfe113e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEvents.java
+@@ -0,0 +1,14 @@
++package io.papermc.paper.registry.event;
++
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * Holds providers for {@link RegistryEntryAddEvent} and {@link RegistryFreezeEvent}
++ * handlers for each applicable registry.
++ */
++public final class RegistryEvents {
++
++ private RegistryEvents() {
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEvent.java b/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..12ec7e794a5047a30354a485acd40fa0f3438eea
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryFreezeEvent.java
+@@ -0,0 +1,39 @@
++package io.papermc.paper.registry.event;
++
++import io.papermc.paper.registry.RegistryBuilder;
++import io.papermc.paper.registry.tag.Tag;
++import io.papermc.paper.registry.tag.TagKey;
++import org.bukkit.Keyed;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * Event object for {@link RegistryEventProvider#freeze()}. This
++ * event is fired right before a registry is frozen disallowing further changes.
++ * It provides a way for plugins to add new objects to the registry.
++ *
++ * @param <T> registry entry type
++ * @param <B> registry entry builder type
++ */
++public interface RegistryFreezeEvent<T, B extends RegistryBuilder<T>> extends RegistryEvent<T> {
++
++ /**
++ * Get the writable registry.
++ *
++ * @return a writable registry
++ */
++ @NonNull WritableRegistry<T, B> registry();
++
++ /**
++ * Gets or creates a tag for the given tag key. This tag
++ * is then required to be filled either from the built-in or
++ * custom datapack.
++ *
++ * @param tagKey the tag key
++ * @return the tag
++ * @param <V> the tag value type
++ */
++ @NonNull <V extends Keyed> Tag<V> getOrCreateTag(@NonNull TagKey<V> tagKey);
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/WritableRegistry.java b/src/main/java/io/papermc/paper/registry/event/WritableRegistry.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..6de377275097f065c38dd59c6db9704018ac81fc
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/WritableRegistry.java
+@@ -0,0 +1,27 @@
++package io.papermc.paper.registry.event;
++
++import io.papermc.paper.registry.RegistryBuilder;
++import io.papermc.paper.registry.TypedKey;
++import java.util.function.Consumer;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * A registry which supports registering new objects.
++ *
++ * @param <T> registry entry type
++ * @param <B> registry entry builder type
++ */
++public interface WritableRegistry<T, B extends RegistryBuilder<T>> {
++
++ /**
++ * Register a new value with the specified key. This will
++ * fire a {@link RegistryEntryAddEvent} for the new entry.
++ *
++ * @param key the entry's key (must be unique from others)
++ * @param value a consumer for the entry's builder
++ */
++ void register(@NonNull TypedKey<T> key, @NonNull Consumer<? super B> value);
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddConfiguration.java b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c5b484ee5f4a0f283a8d46f5490afac4b58b06be
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddConfiguration.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.configuration.PrioritizedLifecycleEventHandlerConfiguration;
++import io.papermc.paper.registry.TypedKey;
++import java.util.function.Predicate;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.Contract;
++
++/**
++ * Specific configuration for {@link io.papermc.paper.registry.event.RegistryEntryAddEvent}s.
++ *
++ * @param <T> registry entry type
++ */
++public interface RegistryEntryAddConfiguration<T> extends PrioritizedLifecycleEventHandlerConfiguration<BootstrapContext> {
++
++ /**
++ * Only call the handler if the value being added matches the specified key.
++ *
++ * @param key the key to match
++ * @return this configuration
++ */
++ @Contract(value = "_ -> this", mutates = "this")
++ default @NonNull RegistryEntryAddConfiguration<T> filter(final @NonNull TypedKey<T> key) {
++ return this.filter(key::equals);
++ }
++
++ /**
++ * Only call the handler if the value being added passes the provided filter.
++ *
++ * @param filter the predicate to match the key against
++ * @return this configuration
++ */
++ @Contract(value = "_ -> this", mutates = "this")
++ @NonNull RegistryEntryAddConfiguration<T> filter(@NonNull Predicate<TypedKey<T>> filter);
++
++ @Override
++ @NonNull RegistryEntryAddConfiguration<T> priority(int priority);
++
++ @Override
++ @NonNull RegistryEntryAddConfiguration<T> monitor();
++}
+diff --git a/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventType.java b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventType.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..f4d4ebf6cbed1b4a9955ceb2d0586782181d97e5
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventType.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.LifecycleEventType;
++import io.papermc.paper.registry.RegistryBuilder;
++import io.papermc.paper.registry.event.RegistryEntryAddEvent;
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * Lifecycle event type for {@link RegistryEntryAddEvent}s.
++ *
++ * @param <T> registry entry type
++ * @param <B> registry entry builder type
++ */
++public interface RegistryEntryAddEventType<T, B extends RegistryBuilder<T>> extends LifecycleEventType<BootstrapContext, RegistryEntryAddEvent<T, B>, RegistryEntryAddConfiguration<T>> {
++}
+diff --git a/src/main/java/io/papermc/paper/registry/set/RegistryKeySet.java b/src/main/java/io/papermc/paper/registry/set/RegistryKeySet.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b891101b43148f63c96b7dd611914c85d7b29dbf
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/set/RegistryKeySet.java
+@@ -0,0 +1,50 @@
++package io.papermc.paper.registry.set;
++
++import io.papermc.paper.registry.TypedKey;
++import java.util.Collection;
++import java.util.Iterator;
++import org.bukkit.Keyed;
++import org.bukkit.Registry;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Unmodifiable;
++
++public non-sealed interface RegistryKeySet<T extends Keyed> extends Iterable<TypedKey<T>>, RegistrySet<T> { // TODO remove Keyed
++
++ @Override
++ default int size() {
++ return this.values().size();
++ }
++
++ /**
++ * Get the keys for the values in this set.
++ *
++ * @return the keys
++ */
++ @NonNull @Unmodifiable Collection<TypedKey<T>> values();
++
++ /**
++ * Resolve this set into a collection of values. Prefer using
++ * {@link #values()}.
++ *
++ * @param registry the registry to resolve the values from (must match {@link #registryKey()})
++ * @return the resolved values
++ * @see RegistryKeySet#values()
++ */
++ @NonNull @Unmodifiable Collection<T> resolve(final @NonNull Registry<T> registry);
++
++ /**
++ * Checks if this set contains the value with the given key.
++ *
++ * @param valueKey the key to check
++ * @return true if the value is in this set
++ */
++ boolean contains(@NonNull TypedKey<T> valueKey);
++
++ @Override
++ default @NonNull Iterator<TypedKey<T>> iterator() {
++ return this.values().iterator();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/set/RegistryKeySetImpl.java b/src/main/java/io/papermc/paper/registry/set/RegistryKeySetImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c712181ad4c6a9d00bc04f8a48515a388c692f48
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/set/RegistryKeySetImpl.java
+@@ -0,0 +1,53 @@
++package io.papermc.paper.registry.set;
++
++import com.google.common.base.Preconditions;
++import io.papermc.paper.registry.RegistryAccess;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.TypedKey;
++import java.util.ArrayList;
++import java.util.Collection;
++import java.util.Collections;
++import java.util.List;
++import org.bukkit.Keyed;
++import org.bukkit.NamespacedKey;
++import org.bukkit.Registry;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Nullable;
++
++@DefaultQualifier(NonNull.class)
++record RegistryKeySetImpl<T extends Keyed>(RegistryKey<T> registryKey, List<TypedKey<T>> values) implements RegistryKeySet<T> { // TODO remove Keyed
++
++ static <T extends Keyed> RegistryKeySet<T> create(final RegistryKey<T> registryKey, final Iterable<? extends T> values) { // TODO remove Keyed
++ final Registry<T> registry = RegistryAccess.registryAccess().getRegistry(registryKey);
++ final ArrayList<TypedKey<T>> keys = new ArrayList<>();
++ for (final T value : values) {
++ final @Nullable NamespacedKey key = registry.getKey(value);
++ Preconditions.checkArgument(key != null, value + " does not have a key in " + registryKey);
++ keys.add(TypedKey.create(registryKey, key));
++ }
++ return new RegistryKeySetImpl<>(registryKey, keys);
++ }
++
++ RegistryKeySetImpl {
++ values = List.copyOf(values);
++ }
++
++ @Override
++ public boolean contains(final TypedKey<T> valueKey) {
++ return this.values.contains(valueKey);
++ }
++
++ @Override
++ public Collection<T> resolve(final Registry<T> registry) {
++ final List<T> values = new ArrayList<>(this.values.size());
++ for (final TypedKey<T> key : this.values) {
++ final @Nullable T value = registry.get(key.key());
++ Preconditions.checkState(value != null, "Trying to access unbound TypedKey: " + key);
++ values.add(value);
++ }
++ return Collections.unmodifiableList(values);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/set/RegistrySet.java b/src/main/java/io/papermc/paper/registry/set/RegistrySet.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..108df480e16131781a52c7bbba2ca6581e4c1ca1
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/set/RegistrySet.java
+@@ -0,0 +1,112 @@
++package io.papermc.paper.registry.set;
++
++import com.google.common.collect.Lists;
++import io.papermc.paper.registry.RegistryKey;
++import io.papermc.paper.registry.TypedKey;
++import io.papermc.paper.registry.tag.Tag;
++import org.bukkit.Keyed;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Contract;
++
++/**
++ * Represents a collection tied to a registry.
++ * <p>
++ * There are 2<!--3--> types of registry sets:
++ * <ul>
++ * <li>{@link Tag} which is a tag from vanilla or a datapack.
++ * These are obtained via {@link org.bukkit.Registry#getTag(io.papermc.paper.registry.tag.TagKey)}.</li>
++ * <li>{@link RegistryKeySet} which is a set of of keys linked to values that are present in the registry. These are
++ * created via {@link #keySet(RegistryKey, Iterable)} or {@link #keySetFromValues(RegistryKey, Iterable)}.</li>
++ * <!-- <li>{@link RegistryValueSet} which is a set of values which are anonymous (don't have keys in the registry). These are
++ * created via {@link #valueSet(RegistryKey, Iterable)}.</li>-->
++ * </ul>
++ *
++ * @param <T> registry value type
++ */
++public sealed interface RegistrySet<T> permits RegistryKeySet, RegistryValueSet {
++
++ // TODO uncomment when direct holder sets need to be exposed to the API
++ // /**
++ // * Creates a {@link RegistryValueSet} from anonymous values.
++ // * <p>All values provided <b>must not</b> have keys in the given registry.</p>
++ // *
++ // * @param registryKey the registry key for the type of these values
++ // * @param values the values
++ // * @return a new registry set
++ // * @param <T> the type of the values
++ // */
++ // @Contract(value = "_, _ -> new", pure = true)
++ // static <T> @NonNull RegistryValueSet<T> valueSet(final @NonNull RegistryKey<T> registryKey, final @NonNull Iterable<? extends T> values) {
++ // return RegistryValueSetImpl.create(registryKey, values);
++ // }
++
++ /**
++ * Creates a {@link RegistryKeySet} from registry-backed values.
++ * <p>All values provided <b>must</b> have keys in the given registry.
++ * <!--For anonymous values, use {@link #valueSet(RegistryKey, Iterable)}--></p>
++ * <p>If references to actual objects are not available yet, use {@link #keySet(RegistryKey, Iterable)} to
++ * create an equivalent {@link RegistryKeySet} using just {@link TypedKey TypedKeys}.</p>
++ *
++ * @param registryKey the registry key for the owner of these values
++ * @param values the values
++ * @return a new registry set
++ * @param <T> the type of the values
++ * @throws IllegalArgumentException if the registry isn't available yet or if any value doesn't have a key in that registry
++ */
++ @Contract(value = "_, _ -> new", pure = true)
++ static <T extends Keyed> @NonNull RegistryKeySet<T> keySetFromValues(final @NonNull RegistryKey<T> registryKey, final @NonNull Iterable<? extends T> values) { // TODO remove Keyed
++ return RegistryKeySetImpl.create(registryKey, values);
++ }
++
++ /**
++ * Creates a direct {@link RegistrySet} from {@link TypedKey TypedKeys}.
++ *
++ * @param registryKey the registry key for the owner of these keys
++ * @param keys the keys for the values
++ * @return a new registry set
++ * @param <T> the type of the values
++ */
++ @SafeVarargs
++ static <T extends Keyed> RegistryKeySet<T> keySet(final @NonNull RegistryKey<T> registryKey, final @NonNull TypedKey<T> @NonNull... keys) { // TODO remove Keyed
++ return keySet(registryKey, Lists.newArrayList(keys));
++ }
++
++ /**
++ * Creates a direct {@link RegistrySet} from {@link TypedKey TypedKeys}.
++ *
++ * @param registryKey the registry key for the owner of these keys
++ * @param keys the keys for the values
++ * @return a new registry set
++ * @param <T> the type of the values
++ */
++ @SuppressWarnings("BoundedWildcard")
++ @Contract(value = "_, _ -> new", pure = true)
++ static <T extends Keyed> @NonNull RegistryKeySet<T> keySet(final @NonNull RegistryKey<T> registryKey, final @NonNull Iterable<TypedKey<T>> keys) { // TODO remove Keyed
++ return new RegistryKeySetImpl<>(registryKey, Lists.newArrayList(keys));
++ }
++
++ /**
++ * Get the registry key for this set.
++ *
++ * @return the registry key
++ */
++ @NonNull RegistryKey<T> registryKey();
++
++ /**
++ * Get the size of this set.
++ *
++ * @return the size
++ */
++ int size();
++
++ /**
++ * Checks if the registry set is empty.
++ *
++ * @return true, if empty
++ */
++ default boolean isEmpty() {
++ return this.size() == 0;
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/set/RegistryValueSet.java b/src/main/java/io/papermc/paper/registry/set/RegistryValueSet.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..58e2e42737d48b243854466eb7f7d3a844a86b6e
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/set/RegistryValueSet.java
+@@ -0,0 +1,34 @@
++package io.papermc.paper.registry.set;
++
++import java.util.Collection;
++import java.util.Iterator;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Unmodifiable;
++
++/**
++ * A collection of anonymous values relating to a registry. These
++ * are values of the same type as the registry, but will not be found
++ * in the registry, hence, anonymous.
++ * @param <T> registry value type
++ */
++public sealed interface RegistryValueSet<T> extends Iterable<T>, RegistrySet<T> permits RegistryValueSetImpl {
++
++ @Override
++ default int size() {
++ return this.values().size();
++ }
++
++ /**
++ * Get the collection of values in this direct set.
++ *
++ * @return the values
++ */
++ @NonNull @Unmodifiable Collection<T> values();
++
++ @Override
++ default @NonNull Iterator<T> iterator() {
++ return this.values().iterator();
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/set/RegistryValueSetImpl.java b/src/main/java/io/papermc/paper/registry/set/RegistryValueSetImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4ce5b26a1fcaae7b28ac8ed3c25014b66c266318
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/set/RegistryValueSetImpl.java
+@@ -0,0 +1,18 @@
++package io.papermc.paper.registry.set;
++
++import com.google.common.collect.Lists;
++import io.papermc.paper.registry.RegistryKey;
++import java.util.List;
++import org.jetbrains.annotations.ApiStatus;
++
++record RegistryValueSetImpl<T>(RegistryKey<T> registryKey, List<T> values) implements RegistryValueSet<T> {
++
++ RegistryValueSetImpl {
++ values = List.copyOf(values);
++ }
++
++ static <T> RegistryValueSet<T> create(final RegistryKey<T> registryKey, final Iterable<? extends T> values) {
++ return new RegistryValueSetImpl<>(registryKey, Lists.newArrayList(values));
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/registry/tag/Tag.java b/src/main/java/io/papermc/paper/registry/tag/Tag.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ae374f68ef9baa16ed90c371f1622de0c0159873
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/tag/Tag.java
+@@ -0,0 +1,25 @@
++package io.papermc.paper.registry.tag;
++
++import io.papermc.paper.registry.set.RegistryKeySet;
++import org.bukkit.Keyed;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * A named {@link RegistryKeySet} which are created
++ * via the datapack tag system.
++ *
++ * @param <T>
++ * @see org.bukkit.Tag
++ * @see org.bukkit.Registry#getTag(TagKey)
++ */
++public interface Tag<T extends Keyed> extends RegistryKeySet<T> { // TODO remove Keyed
++
++ /**
++ * Get the identifier for this named set.
++ *
++ * @return the tag key identifier
++ */
++ @NonNull TagKey<T> tagKey();
++}
+diff --git a/src/main/java/io/papermc/paper/registry/tag/TagKey.java b/src/main/java/io/papermc/paper/registry/tag/TagKey.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a49d328e95f7fda6567ee6c4f5f1878a2c187277
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/tag/TagKey.java
+@@ -0,0 +1,32 @@
++package io.papermc.paper.registry.tag;
++
++import io.papermc.paper.registry.RegistryKey;
++import net.kyori.adventure.key.Key;
++import net.kyori.adventure.key.Keyed;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Contract;
++
++public sealed interface TagKey<T> extends Keyed permits TagKeyImpl {
++
++ /**
++ * Creates a new tag key for a registry.
++ *
++ * @param registryKey the registry for the tag
++ * @param key the specific key for the tag
++ * @return a new tag key
++ * @param <T> the registry value type
++ */
++ @Contract(value = "_, _ -> new", pure = true)
++ static <T> @NonNull TagKey<T> create(final @NonNull RegistryKey<T> registryKey, final @NonNull Key key) {
++ return new TagKeyImpl<>(registryKey, key);
++ }
++
++ /**
++ * Get the registry key for this tag key.
++ *
++ * @return the registry key
++ */
++ @NonNull RegistryKey<T> registryKey();
++}
+diff --git a/src/main/java/io/papermc/paper/registry/tag/TagKeyImpl.java b/src/main/java/io/papermc/paper/registry/tag/TagKeyImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..11d19e339c7c62f2eb4467277552c27e4e83069c
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/tag/TagKeyImpl.java
+@@ -0,0 +1,12 @@
++package io.papermc.paper.registry.tag;
++
++import io.papermc.paper.registry.RegistryKey;
++import net.kyori.adventure.key.Key;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++import org.jetbrains.annotations.ApiStatus;
++
++@DefaultQualifier(NonNull.class)
++record TagKeyImpl<T>(RegistryKey<T> registryKey, Key key) implements TagKey<T> {
++}
+diff --git a/src/main/java/org/bukkit/Registry.java b/src/main/java/org/bukkit/Registry.java
+index 27b987db385a594fede4e884b6437dc363f6e817..9725580b6458e5d37fbc6059869604f9883bd6d1 100644
+--- a/src/main/java/org/bukkit/Registry.java
++++ b/src/main/java/org/bukkit/Registry.java
+@@ -357,6 +357,27 @@ public interface Registry<T extends Keyed> extends Iterable<T> {
+ */
+ @Nullable
+ T get(@NotNull NamespacedKey key);
++ // Paper start
++ /**
++ * Get the object by its key.
++ *
++ * @param key non-null key
++ * @return item or null if it does not exist
++ */
++ default @Nullable T get(final net.kyori.adventure.key.@NotNull Key key) {
++ return key instanceof final NamespacedKey nsKey ? this.get(nsKey) : this.get(new NamespacedKey(key.namespace(), key.value()));
++ }
++
++ /**
++ * Get the object by its typed key.
++ *
++ * @param typedKey non-null typed key
++ * @return item or null if it does not exist
++ */
++ default @Nullable T get(final io.papermc.paper.registry.@NotNull TypedKey<T> typedKey) {
++ return this.get(typedKey.key());
++ }
++ // Paper end
+
+ // Paper start - improve Registry
+ /**
+@@ -431,6 +452,34 @@ public interface Registry<T extends Keyed> extends Iterable<T> {
+ }
+ // Paper end - improve Registry
+
++ // Paper start - RegistrySet API
++ /**
++ * Checks if this registry has a tag with the given key.
++ *
++ * @param key the key to check for
++ * @return true if this registry has a tag with the given key, false otherwise
++ * @see #getTag(io.papermc.paper.registry.tag.TagKey)
++ */
++ @ApiStatus.Experimental
++ default boolean hasTag(final io.papermc.paper.registry.tag.@NotNull TagKey<T> key) {
++ throw new UnsupportedOperationException(this + " doesn't have tags");
++ }
++
++ /**
++ * Gets the named registry set (tag) for the given key.
++ *
++ * @param key the key to get the tag for
++ * @return the tag for the key
++ * @throws java.util.NoSuchElementException if no tag with the given key is found
++ * @throws UnsupportedOperationException if this registry doesn't have or support tags
++ * @see #hasTag(io.papermc.paper.registry.tag.TagKey)
++ */
++ @ApiStatus.Experimental
++ default @NotNull io.papermc.paper.registry.tag.Tag<T> getTag(final io.papermc.paper.registry.tag.@NotNull TagKey<T> key) {
++ throw new UnsupportedOperationException(this + " doesn't have tags");
++ }
++ // Paper end - RegistrySet API
++
+ /**
+ * Returns a new stream, which contains all registry items, which are registered to the registry.
+ *
+@@ -512,5 +561,23 @@ public interface Registry<T extends Keyed> extends Iterable<T> {
+ return value.getKey();
+ }
+ // Paper end - improve Registry
++
++ // Paper start - RegistrySet API
++ @SuppressWarnings("deprecation")
++ @Override
++ public boolean hasTag(final io.papermc.paper.registry.tag.@NotNull TagKey<T> key) {
++ return Bukkit.getUnsafe().getTag(key) != null;
++ }
++
++ @SuppressWarnings("deprecation")
++ @Override
++ public io.papermc.paper.registry.tag.@NotNull Tag<T> getTag(final io.papermc.paper.registry.tag.@NotNull TagKey<T> key) {
++ final io.papermc.paper.registry.tag.Tag<T> tag = Bukkit.getUnsafe().getTag(key);
++ if (tag == null) {
++ throw new java.util.NoSuchElementException("No tag " + key + " found");
++ }
++ return tag;
++ }
++ // Paper end - RegistrySet API
+ }
+ }
+diff --git a/src/main/java/org/bukkit/UnsafeValues.java b/src/main/java/org/bukkit/UnsafeValues.java
+index 0e9ccfee7a03d341e7c4d271f53b4ed168b404ef..7332034bb1753f48f7904dafab1ef4b3ee117ea3 100644
+--- a/src/main/java/org/bukkit/UnsafeValues.java
++++ b/src/main/java/org/bukkit/UnsafeValues.java
+@@ -275,4 +275,6 @@ public interface UnsafeValues {
+ // Paper end - lifecycle event API
+
+ @NotNull java.util.List<net.kyori.adventure.text.Component> computeTooltipLines(@NotNull ItemStack itemStack, @NotNull io.papermc.paper.inventory.tooltip.TooltipContext tooltipContext, @Nullable org.bukkit.entity.Player player); // Paper - expose itemstack tooltip lines
++
++ <A extends Keyed, M> io.papermc.paper.registry.tag.@Nullable Tag<A> getTag(io.papermc.paper.registry.tag.@NotNull TagKey<A> tagKey); // Paper - hack to get tags for non-server backed registries
+ }
diff --git a/patches/api/0476-WIP-Tag-API.patch b/patches/api/0476-WIP-Tag-API.patch
deleted file mode 100644
index 32ed16f1cf..0000000000
--- a/patches/api/0476-WIP-Tag-API.patch
+++ /dev/null
@@ -1,62 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Owen1212055 <[email protected]>
-Date: Sat, 15 Jun 2024 21:42:19 -0400
-Subject: [PATCH] WIP Tag API
-
-
-diff --git a/src/main/java/io/papermc/paper/registry/tag/TagKey.java b/src/main/java/io/papermc/paper/registry/tag/TagKey.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a49d328e95f7fda6567ee6c4f5f1878a2c187277
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/registry/tag/TagKey.java
-@@ -0,0 +1,32 @@
-+package io.papermc.paper.registry.tag;
-+
-+import io.papermc.paper.registry.RegistryKey;
-+import net.kyori.adventure.key.Key;
-+import net.kyori.adventure.key.Keyed;
-+import org.checkerframework.checker.nullness.qual.NonNull;
-+import org.jetbrains.annotations.ApiStatus;
-+import org.jetbrains.annotations.Contract;
-+
-+public sealed interface TagKey<T> extends Keyed permits TagKeyImpl {
-+
-+ /**
-+ * Creates a new tag key for a registry.
-+ *
-+ * @param registryKey the registry for the tag
-+ * @param key the specific key for the tag
-+ * @return a new tag key
-+ * @param <T> the registry value type
-+ */
-+ @Contract(value = "_, _ -> new", pure = true)
-+ static <T> @NonNull TagKey<T> create(final @NonNull RegistryKey<T> registryKey, final @NonNull Key key) {
-+ return new TagKeyImpl<>(registryKey, key);
-+ }
-+
-+ /**
-+ * Get the registry key for this tag key.
-+ *
-+ * @return the registry key
-+ */
-+ @NonNull RegistryKey<T> registryKey();
-+}
-diff --git a/src/main/java/io/papermc/paper/registry/tag/TagKeyImpl.java b/src/main/java/io/papermc/paper/registry/tag/TagKeyImpl.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..11d19e339c7c62f2eb4467277552c27e4e83069c
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/registry/tag/TagKeyImpl.java
-@@ -0,0 +1,12 @@
-+package io.papermc.paper.registry.tag;
-+
-+import io.papermc.paper.registry.RegistryKey;
-+import net.kyori.adventure.key.Key;
-+import org.checkerframework.checker.nullness.qual.NonNull;
-+import org.checkerframework.framework.qual.DefaultQualifier;
-+import org.jetbrains.annotations.ApiStatus;
-+
-+@DefaultQualifier(NonNull.class)
-+record TagKeyImpl<T>(RegistryKey<T> registryKey, Key key) implements TagKey<T> {
-+}
diff --git a/patches/server/0010-Adventure.patch b/patches/server/0010-Adventure.patch
index 75e61f1b24..c6a160a49b 100644
--- a/patches/server/0010-Adventure.patch
+++ b/patches/server/0010-Adventure.patch
@@ -1157,15 +1157,16 @@ index 0000000000000000000000000000000000000000..2fd6c3e65354071af71c7d8ebb97b559
+}
diff --git a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
new file mode 100644
-index 0000000000000000000000000000000000000000..a6aef1ac31f3d2784b5d7b1af616965b5cd2c383
+index 0000000000000000000000000000000000000000..dc4837c577676115f0653acc35f55962a432e425
--- /dev/null
+++ b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
-@@ -0,0 +1,478 @@
+@@ -0,0 +1,479 @@
+package io.papermc.paper.adventure;
+
+import com.mojang.brigadier.StringReader;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.serialization.JavaOps;
++import com.mojang.serialization.JsonOps;
+import io.netty.util.AttributeKey;
+import java.io.IOException;
+import java.util.ArrayList;
@@ -1302,7 +1303,7 @@ index 0000000000000000000000000000000000000000..a6aef1ac31f3d2784b5d7b1af616965b
+ return decoded.toString();
+ }
+ };
-+ public static final ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> WRAPPER_AWARE_SERIALIZER = new WrapperAwareSerializer();
++ public static final ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> WRAPPER_AWARE_SERIALIZER = new WrapperAwareSerializer(() -> CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE));
+
+ private PaperAdventure() {
+ }
@@ -1641,27 +1642,36 @@ index 0000000000000000000000000000000000000000..a6aef1ac31f3d2784b5d7b1af616965b
+}
diff --git a/src/main/java/io/papermc/paper/adventure/WrapperAwareSerializer.java b/src/main/java/io/papermc/paper/adventure/WrapperAwareSerializer.java
new file mode 100644
-index 0000000000000000000000000000000000000000..c786ddf0ef19757011452204fd11d24541c39d9e
+index 0000000000000000000000000000000000000000..a16344476abbb4f3e8aac26d4add9da53b7fc7df
--- /dev/null
+++ b/src/main/java/io/papermc/paper/adventure/WrapperAwareSerializer.java
-@@ -0,0 +1,34 @@
+@@ -0,0 +1,43 @@
+package io.papermc.paper.adventure;
+
++import com.google.common.base.Suppliers;
+import com.mojang.datafixers.util.Pair;
+import com.mojang.serialization.JavaOps;
++import java.util.function.Supplier;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.ComponentSerializer;
+import net.minecraft.network.chat.ComponentSerialization;
+import net.minecraft.resources.RegistryOps;
+import org.bukkit.craftbukkit.CraftRegistry;
+
-+final class WrapperAwareSerializer implements ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> {
++public final class WrapperAwareSerializer implements ComponentSerializer<Component, Component, net.minecraft.network.chat.Component> {
++
++ private final Supplier<RegistryOps<Object>> javaOps;
++
++ public WrapperAwareSerializer(final Supplier<RegistryOps<Object>> javaOps) {
++ this.javaOps = Suppliers.memoize(javaOps::get);
++ }
++
+ @Override
+ public Component deserialize(final net.minecraft.network.chat.Component input) {
+ if (input instanceof AdventureComponent) {
+ return ((AdventureComponent) input).adventure;
+ }
-+ final RegistryOps<Object> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE);
++ final RegistryOps<Object> ops = this.javaOps.get();
+ final Object obj = ComponentSerialization.CODEC.encodeStart(ops, input)
+ .getOrThrow(s -> new RuntimeException("Failed to encode Minecraft Component: " + input + "; " + s));
+ final Pair<Component, Object> converted = AdventureCodecs.COMPONENT_CODEC.decode(ops, obj)
@@ -1671,7 +1681,7 @@ index 0000000000000000000000000000000000000000..c786ddf0ef19757011452204fd11d245
+
+ @Override
+ public net.minecraft.network.chat.Component serialize(final Component component) {
-+ final RegistryOps<Object> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JavaOps.INSTANCE);
++ final RegistryOps<Object> ops = this.javaOps.get();
+ final Object obj = AdventureCodecs.COMPONENT_CODEC.encodeStart(ops, component)
+ .getOrThrow(s -> new RuntimeException("Failed to encode adventure Component: " + component + "; " + s));
+ final Pair<net.minecraft.network.chat.Component, Object> converted = ComponentSerialization.CODEC.decode(ops, obj)
diff --git a/patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch b/patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch
index cefaa74db4..8e5fbfaf15 100644
--- a/patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch
+++ b/patches/server/0011-Use-TerminalConsoleAppender-for-console-improvements.patch
@@ -216,10 +216,10 @@ index 0000000000000000000000000000000000000000..8f07539a82f449ad217e316a7513a170
+
+}
diff --git a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
-index a6aef1ac31f3d2784b5d7b1af616965b5cd2c383..2390618c553dec2f32467dd8f76a6e4651f726c9 100644
+index dc4837c577676115f0653acc35f55962a432e425..22fe529890f34f66534c01248f654dc911b44c3b 100644
--- a/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
+++ b/src/main/java/io/papermc/paper/adventure/PaperAdventure.java
-@@ -31,6 +31,7 @@ import net.kyori.adventure.text.flattener.ComponentFlattener;
+@@ -32,6 +32,7 @@ import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.serializer.ComponentSerializer;
@@ -227,7 +227,7 @@ index a6aef1ac31f3d2784b5d7b1af616965b5cd2c383..2390618c553dec2f32467dd8f76a6e46
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
-@@ -128,6 +129,7 @@ public final class PaperAdventure {
+@@ -129,6 +130,7 @@ public final class PaperAdventure {
public static final AttributeKey<Locale> LOCALE_ATTRIBUTE = AttributeKey.valueOf("adventure:locale"); // init after FLATTENER because classloading triggered here might create a logger
@Deprecated
public static final PlainComponentSerializer PLAIN = PlainComponentSerializer.builder().flattener(FLATTENER).build();
diff --git a/patches/server/0019-Paper-Plugins.patch b/patches/server/0019-Paper-Plugins.patch
index 17df0a46b5..8bb43515e2 100644
--- a/patches/server/0019-Paper-Plugins.patch
+++ b/patches/server/0019-Paper-Plugins.patch
@@ -760,16 +760,18 @@ index 0000000000000000000000000000000000000000..b38e1e0f3d3055086f51bb191fd4b60e
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java
new file mode 100644
-index 0000000000000000000000000000000000000000..6c0f2c315387734f8dd4a7eca633aa0a9856dd17
+index 0000000000000000000000000000000000000000..48bc745ca9632fc46b5f786ff570434702eb47f2
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java
-@@ -0,0 +1,65 @@
+@@ -0,0 +1,74 @@
+package io.papermc.paper.plugin.entrypoint;
+
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.storage.BootstrapProviderStorage;
+import io.papermc.paper.plugin.storage.ProviderStorage;
+import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
++import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
++import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.HashMap;
@@ -782,9 +784,11 @@ index 0000000000000000000000000000000000000000..6c0f2c315387734f8dd4a7eca633aa0a
+
+ public static final LaunchEntryPointHandler INSTANCE = new LaunchEntryPointHandler();
+ private final Map<Entrypoint<?>, ProviderStorage<?>> storage = new HashMap<>();
++ private final Object2BooleanMap<Entrypoint<?>> enteredMap = new Object2BooleanOpenHashMap<>();
+
+ LaunchEntryPointHandler() {
+ this.populateProviderStorage();
++ this.enteredMap.defaultReturnValue(false);
+ }
+
+ // Utility
@@ -800,6 +804,7 @@ index 0000000000000000000000000000000000000000..6c0f2c315387734f8dd4a7eca633aa0a
+ }
+
+ storage.enter();
++ this.enteredMap.put(entrypoint, true);
+ }
+
+ @Override
@@ -823,6 +828,10 @@ index 0000000000000000000000000000000000000000..6c0f2c315387734f8dd4a7eca633aa0a
+ return storage;
+ }
+
++ public boolean hasEntered(Entrypoint<?> entrypoint) {
++ return this.enteredMap.getBoolean(entrypoint);
++ }
++
+ // Reload only
+ public void populateProviderStorage() {
+ this.storage.put(Entrypoint.BOOTSTRAPPER, new BootstrapProviderStorage());
diff --git a/patches/server/0237-Optimize-MappedRegistry.patch b/patches/server/0237-Optimize-MappedRegistry.patch
index 8f45008c5f..17c68f31d7 100644
--- a/patches/server/0237-Optimize-MappedRegistry.patch
+++ b/patches/server/0237-Optimize-MappedRegistry.patch
@@ -8,7 +8,7 @@ Use larger initial sizes to increase bucket capacity on the BiMap
BiMap.get was seen to be using a good bit of CPU time.
diff --git a/src/main/java/net/minecraft/core/MappedRegistry.java b/src/main/java/net/minecraft/core/MappedRegistry.java
-index 362e49f503f3c792fbecf41ec9f235bbc02644de..6e4d5c168acdb9aaa9fbbee090082e4dc25e89e9 100644
+index 1dcbde18bd9c462cca48887b904a9c43261e1854..edbbafd1705345282e5e6251eb71bfde5793b7d4 100644
--- a/src/main/java/net/minecraft/core/MappedRegistry.java
+++ b/src/main/java/net/minecraft/core/MappedRegistry.java
@@ -35,11 +35,11 @@ public class MappedRegistry<T> implements WritableRegistry<T> {
diff --git a/patches/server/0475-Add-RegistryAccess-for-managing-Registries.patch b/patches/server/0475-Add-RegistryAccess-for-managing-Registries.patch
index 7c5dbe8250..5a0885dc09 100644
--- a/patches/server/0475-Add-RegistryAccess-for-managing-Registries.patch
+++ b/patches/server/0475-Add-RegistryAccess-for-managing-Registries.patch
@@ -12,12 +12,13 @@ public net.minecraft.server.RegistryLayer STATIC_ACCESS
diff --git a/src/main/java/io/papermc/paper/registry/PaperRegistries.java b/src/main/java/io/papermc/paper/registry/PaperRegistries.java
new file mode 100644
-index 0000000000000000000000000000000000000000..c1ee87876af79d0fcacd7b930d17d110464ac9d1
+index 0000000000000000000000000000000000000000..1e098dc25bd338ff179491ff3382ac56aad9948e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/registry/PaperRegistries.java
-@@ -0,0 +1,122 @@
+@@ -0,0 +1,133 @@
+package io.papermc.paper.registry;
+
++import io.papermc.paper.adventure.PaperAdventure;
+import io.papermc.paper.registry.entry.RegistryEntry;
+import java.util.Collections;
+import java.util.IdentityHashMap;
@@ -48,6 +49,7 @@ index 0000000000000000000000000000000000000000..c1ee87876af79d0fcacd7b930d17d110
+import org.bukkit.craftbukkit.inventory.trim.CraftTrimPattern;
+import org.bukkit.craftbukkit.legacy.FieldRename;
+import org.bukkit.craftbukkit.potion.CraftPotionEffectType;
++import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+import org.bukkit.damage.DamageType;
+import org.bukkit.entity.Wolf;
+import org.bukkit.entity.memory.MemoryKey;
@@ -66,9 +68,9 @@ index 0000000000000000000000000000000000000000..c1ee87876af79d0fcacd7b930d17d110
+@DefaultQualifier(NonNull.class)
+public final class PaperRegistries {
+
-+ static final List<RegistryEntry<?, ?, ?>> REGISTRY_ENTRIES;
-+ private static final Map<RegistryKey<?>, RegistryEntry<?, ?, ?>> BY_REGISTRY_KEY;
-+ private static final Map<ResourceKey<?>, RegistryEntry<?, ?, ?>> BY_RESOURCE_KEY;
++ static final List<RegistryEntry<?, ?>> REGISTRY_ENTRIES;
++ private static final Map<RegistryKey<?>, RegistryEntry<?, ?>> BY_REGISTRY_KEY;
++ private static final Map<ResourceKey<?>, RegistryEntry<?, ?>> BY_RESOURCE_KEY;
+ static {
+ REGISTRY_ENTRIES = List.of(
+ // built-ins
@@ -105,9 +107,9 @@ index 0000000000000000000000000000000000000000..c1ee87876af79d0fcacd7b930d17d110
+ apiOnly(Registries.FROG_VARIANT, RegistryKey.FROG_VARIANT, () -> org.bukkit.Registry.FROG_VARIANT),
+ apiOnly(Registries.MAP_DECORATION_TYPE, RegistryKey.MAP_DECORATION_TYPE, () -> org.bukkit.Registry.MAP_DECORATION_TYPE)
+ );
-+ final Map<RegistryKey<?>, RegistryEntry<?, ?, ?>> byRegistryKey = new IdentityHashMap<>(REGISTRY_ENTRIES.size());
-+ final Map<ResourceKey<?>, RegistryEntry<?, ?, ?>> byResourceKey = new IdentityHashMap<>(REGISTRY_ENTRIES.size());
-+ for (final RegistryEntry<?, ?, ?> entry : REGISTRY_ENTRIES) {
++ final Map<RegistryKey<?>, RegistryEntry<?, ?>> byRegistryKey = new IdentityHashMap<>(REGISTRY_ENTRIES.size());
++ final Map<ResourceKey<?>, RegistryEntry<?, ?>> byResourceKey = new IdentityHashMap<>(REGISTRY_ENTRIES.size());
++ for (final RegistryEntry<?, ?> entry : REGISTRY_ENTRIES) {
+ byRegistryKey.put(entry.apiKey(), entry);
+ byResourceKey.put(entry.mcKey(), entry);
+ }
@@ -116,31 +118,40 @@ index 0000000000000000000000000000000000000000..c1ee87876af79d0fcacd7b930d17d110
+ }
+
+ @SuppressWarnings("unchecked")
-+ public static <M, T extends Keyed, R extends org.bukkit.Registry<T>> @Nullable RegistryEntry<M, T, R> getEntry(final ResourceKey<? extends Registry<M>> resourceKey) {
-+ return (RegistryEntry<M, T, R>) BY_RESOURCE_KEY.get(resourceKey);
++ public static <M, T extends Keyed> @Nullable RegistryEntry<M, T> getEntry(final ResourceKey<? extends Registry<M>> resourceKey) {
++ return (RegistryEntry<M, T>) BY_RESOURCE_KEY.get(resourceKey);
+ }
+
+ @SuppressWarnings("unchecked")
-+ public static <M, T extends Keyed, R extends org.bukkit.Registry<T>> @Nullable RegistryEntry<M, T, R> getEntry(final RegistryKey<? super T> registryKey) {
-+ return (RegistryEntry<M, T, R>) BY_REGISTRY_KEY.get(registryKey);
++ public static <M, T extends Keyed> @Nullable RegistryEntry<M, T> getEntry(final RegistryKey<? super T> registryKey) {
++ return (RegistryEntry<M, T>) BY_REGISTRY_KEY.get(registryKey);
+ }
+
+ @SuppressWarnings("unchecked")
-+ public static <M, T> RegistryKey<T> fromNms(final ResourceKey<? extends Registry<M>> registryResourceKey) {
++ public static <M, T> RegistryKey<T> registryFromNms(final ResourceKey<? extends Registry<M>> registryResourceKey) {
+ return (RegistryKey<T>) Objects.requireNonNull(BY_RESOURCE_KEY.get(registryResourceKey), registryResourceKey + " doesn't have an api RegistryKey").apiKey();
+ }
+
+ @SuppressWarnings("unchecked")
-+ public static <M, T> ResourceKey<? extends Registry<M>> toNms(final RegistryKey<T> registryKey) {
++ public static <M, T> ResourceKey<? extends Registry<M>> registryToNms(final RegistryKey<T> registryKey) {
+ return (ResourceKey<? extends Registry<M>>) Objects.requireNonNull(BY_REGISTRY_KEY.get(registryKey), registryKey + " doesn't have an mc registry ResourceKey").mcKey();
+ }
+
++ public static <M, T> TypedKey<T> fromNms(final ResourceKey<M> resourceKey) {
++ return TypedKey.create(registryFromNms(resourceKey.registryKey()), CraftNamespacedKey.fromMinecraft(resourceKey.location()));
++ }
++
++ @SuppressWarnings({"unchecked", "RedundantCast"})
++ public static <M, T> ResourceKey<M> toNms(final TypedKey<T> typedKey) {
++ return ResourceKey.create((ResourceKey<? extends Registry<M>>) PaperRegistries.registryToNms(typedKey.registryKey()), PaperAdventure.asVanilla(typedKey.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
new file mode 100644
-index 0000000000000000000000000000000000000000..9f2bcfe0d9e479466a1e46e503071d1151310e6a
+index 0000000000000000000000000000000000000000..d591e3a2e19d5358a0d25a5a681368943622d231
--- /dev/null
+++ b/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java
@@ -0,0 +1,124 @@
@@ -190,7 +201,7 @@ index 0000000000000000000000000000000000000000..9f2bcfe0d9e479466a1e46e503071d11
+ @Override
+ public <T extends Keyed> @Nullable Registry<T> getRegistry(final Class<T> type) {
+ final RegistryKey<T> registryKey;
-+ final @Nullable RegistryEntry<?, T, ?> entry;
++ final @Nullable RegistryEntry<?, T> entry;
+ registryKey = requireNonNull(byType(type), () -> type + " is not a valid registry type");
+ entry = PaperRegistries.getEntry(registryKey);
+ final @Nullable RegistryHolder<T> registry = (RegistryHolder<T>) this.registries.get(registryKey);
@@ -198,7 +209,7 @@ index 0000000000000000000000000000000000000000..9f2bcfe0d9e479466a1e46e503071d11
+ // if the registry exists, return right away. Since this is the "legacy" method, we return DelayedRegistry
+ // for the non-builtin Registry instances stored as fields in Registry.
+ return registry.get();
-+ } else if (entry instanceof DelayedRegistryEntry<?, T, ?>) {
++ } else if (entry instanceof DelayedRegistryEntry<?, T>) {
+ // if the registry doesn't exist and the entry is marked as "delayed", we create a registry holder that is empty
+ // which will later be filled with the actual registry. This is so the fields on org.bukkit.Registry can be populated with
+ // registries that don't exist at the time org.bukkit.Registry is statically initialized.
@@ -243,7 +254,7 @@ index 0000000000000000000000000000000000000000..9f2bcfe0d9e479466a1e46e503071d11
+
+ @SuppressWarnings("unchecked") // this method should be called right after any new MappedRegistry instances are created to later be used by the server.
+ private <M, B extends Keyed, R extends Registry<B>> void registerRegistry(final ResourceKey<? extends net.minecraft.core.Registry<M>> resourceKey, final net.minecraft.core.Registry<M> registry, final boolean replace) {
-+ final @Nullable RegistryEntry<M, B, R> entry = PaperRegistries.getEntry(resourceKey);
++ final @Nullable RegistryEntry<M, B> entry = PaperRegistries.getEntry(resourceKey);
+ if (entry == null) { // skip registries that don't have API entries
+ return;
+ }
@@ -252,7 +263,7 @@ index 0000000000000000000000000000000000000000..9f2bcfe0d9e479466a1e46e503071d11
+ // if the holder doesn't exist yet, or is marked as "replaceable", put it in the map.
+ this.registries.put(entry.apiKey(), entry.createRegistryHolder(registry));
+ } else {
-+ if (registryHolder instanceof RegistryHolder.Delayed<?, ?> && entry instanceof final DelayedRegistryEntry<M, B, R> delayedEntry) {
++ if (registryHolder instanceof RegistryHolder.Delayed<?, ?> && entry instanceof final DelayedRegistryEntry<M, B> delayedEntry) {
+ // if the registry holder is delayed, and the entry is marked as "delayed", then load the holder with the CraftRegistry instance that wraps the actual nms Registry.
+ ((RegistryHolder.Delayed<B, R>) registryHolder).loadFrom(delayedEntry, registry);
+ } else {
@@ -270,7 +281,7 @@ index 0000000000000000000000000000000000000000..9f2bcfe0d9e479466a1e46e503071d11
+}
diff --git a/src/main/java/io/papermc/paper/registry/RegistryHolder.java b/src/main/java/io/papermc/paper/registry/RegistryHolder.java
new file mode 100644
-index 0000000000000000000000000000000000000000..02402ef647c3e78ed56fd6b2687bf7c67448f891
+index 0000000000000000000000000000000000000000..a31bdd9f02fe75a87fceb2ebe8c36b3232a561cc
--- /dev/null
+++ b/src/main/java/io/papermc/paper/registry/RegistryHolder.java
@@ -0,0 +1,47 @@
@@ -312,7 +323,7 @@ index 0000000000000000000000000000000000000000..02402ef647c3e78ed56fd6b2687bf7c6
+ return this.delayedRegistry;
+ }
+
-+ <M> void loadFrom(final DelayedRegistryEntry<M, B, R> delayedEntry, final net.minecraft.core.Registry<M> registry) {
++ <M> void loadFrom(final DelayedRegistryEntry<M, B> delayedEntry, final net.minecraft.core.Registry<M> registry) {
+ final RegistryHolder<B> delegateHolder = delayedEntry.delegate().createRegistryHolder(registry);
+ if (!(delegateHolder instanceof RegistryHolder.Memoized<B, ?>)) {
+ throw new IllegalArgumentException(delegateHolder + " must be a memoized holder");
@@ -323,7 +334,7 @@ index 0000000000000000000000000000000000000000..02402ef647c3e78ed56fd6b2687bf7c6
+}
diff --git a/src/main/java/io/papermc/paper/registry/entry/ApiRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/ApiRegistryEntry.java
new file mode 100644
-index 0000000000000000000000000000000000000000..b2281a21eafd1f22f0ce261787e29af8a8637147
+index 0000000000000000000000000000000000000000..2295b0d145cbaabef5d29482c817575dcbe2ba54
--- /dev/null
+++ b/src/main/java/io/papermc/paper/registry/entry/ApiRegistryEntry.java
@@ -0,0 +1,27 @@
@@ -336,7 +347,7 @@ index 0000000000000000000000000000000000000000..b2281a21eafd1f22f0ce261787e29af8
+import net.minecraft.resources.ResourceKey;
+import org.bukkit.Keyed;
+
-+public class ApiRegistryEntry<M, B extends Keyed> extends BaseRegistryEntry<M, B, org.bukkit.Registry<B>> {
++public class ApiRegistryEntry<M, B extends Keyed> extends BaseRegistryEntry<M, B> {
+
+ private final Supplier<org.bukkit.Registry<B>> registrySupplier;
+
@@ -356,7 +367,7 @@ index 0000000000000000000000000000000000000000..b2281a21eafd1f22f0ce261787e29af8
+}
diff --git a/src/main/java/io/papermc/paper/registry/entry/BaseRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/BaseRegistryEntry.java
new file mode 100644
-index 0000000000000000000000000000000000000000..1be8a5feccd27779fcd8ebb2c362f17d78d307da
+index 0000000000000000000000000000000000000000..ceb217dbbb84e8bd51365dd47bf91971e364d298
--- /dev/null
+++ b/src/main/java/io/papermc/paper/registry/entry/BaseRegistryEntry.java
@@ -0,0 +1,27 @@
@@ -367,7 +378,7 @@ index 0000000000000000000000000000000000000000..1be8a5feccd27779fcd8ebb2c362f17d
+import net.minecraft.resources.ResourceKey;
+import org.bukkit.Keyed;
+
-+public abstract class BaseRegistryEntry<M, B extends Keyed, R extends org.bukkit.Registry<B>> implements RegistryEntry<M, B, R> { // TODO remove Keyed
++public abstract class BaseRegistryEntry<M, B extends Keyed> implements RegistryEntry<M, B> { // TODO remove Keyed
+
+ private final ResourceKey<? extends Registry<M>> minecraftRegistryKey;
+ private final RegistryKey<B> apiRegistryKey;
@@ -389,7 +400,7 @@ index 0000000000000000000000000000000000000000..1be8a5feccd27779fcd8ebb2c362f17d
+}
diff --git a/src/main/java/io/papermc/paper/registry/entry/CraftRegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/CraftRegistryEntry.java
new file mode 100644
-index 0000000000000000000000000000000000000000..46b2560de884ef381cb7fc8669cad8f5a1fa3645
+index 0000000000000000000000000000000000000000..568984894a5463ccfa68bb6944b409ab0a2d7ad7
--- /dev/null
+++ b/src/main/java/io/papermc/paper/registry/entry/CraftRegistryEntry.java
@@ -0,0 +1,49 @@
@@ -408,13 +419,13 @@ index 0000000000000000000000000000000000000000..46b2560de884ef381cb7fc8669cad8f5
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+@DefaultQualifier(NonNull.class)
-+public class CraftRegistryEntry<M, B extends Keyed> extends BaseRegistryEntry<M, B, CraftRegistry<B, M>> { // TODO remove Keyed
++public class CraftRegistryEntry<M, B extends Keyed> extends BaseRegistryEntry<M, B> { // TODO remove Keyed
+
+ private static final BiFunction<NamespacedKey, ApiVersion, NamespacedKey> EMPTY = (namespacedKey, apiVersion) -> namespacedKey;
+
+ protected final Class<?> classToPreload;
+ protected final BiFunction<NamespacedKey, M, B> minecraftToBukkit;
-+ private BiFunction<NamespacedKey, ApiVersion, NamespacedKey> updater = EMPTY;
++ protected BiFunction<NamespacedKey, ApiVersion, NamespacedKey> updater = EMPTY;
+
+ protected CraftRegistryEntry(
+ final ResourceKey<? extends Registry<M>> mcKey,
@@ -428,7 +439,7 @@ index 0000000000000000000000000000000000000000..46b2560de884ef381cb7fc8669cad8f5
+ }
+
+ @Override
-+ public RegistryEntry<M, B, CraftRegistry<B, M>> withSerializationUpdater(final BiFunction<NamespacedKey, ApiVersion, NamespacedKey> updater) {
++ public RegistryEntry<M, B> withSerializationUpdater(final BiFunction<NamespacedKey, ApiVersion, NamespacedKey> updater) {
+ this.updater = updater;
+ return this;
+ }
@@ -444,10 +455,10 @@ index 0000000000000000000000000000000000000000..46b2560de884ef381cb7fc8669cad8f5
+}
diff --git a/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java b/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java
new file mode 100644
-index 0000000000000000000000000000000000000000..c97bda87742852c921d73f4886721f1ee56b0a85
+index 0000000000000000000000000000000000000000..15991bf13894d850f360a520d1815711d25973ec
--- /dev/null
+++ b/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java
-@@ -0,0 +1,52 @@
+@@ -0,0 +1,51 @@
+package io.papermc.paper.registry.entry;
+
+import io.papermc.paper.registry.RegistryHolder;
@@ -459,17 +470,16 @@ index 0000000000000000000000000000000000000000..c97bda87742852c921d73f4886721f1e
+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.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+@DefaultQualifier(NonNull.class)
-+public interface RegistryEntry<M, B extends Keyed, R extends org.bukkit.Registry<B>> extends RegistryEntryInfo<M, B> { // TODO remove Keyed
++public interface RegistryEntry<M, B extends Keyed> extends RegistryEntryInfo<M, B> { // TODO remove Keyed
+
+ RegistryHolder<B> createRegistryHolder(Registry<M> nmsRegistry);
+
-+ default RegistryEntry<M, B, R> withSerializationUpdater(final BiFunction<NamespacedKey, ApiVersion, NamespacedKey> updater) {
++ default RegistryEntry<M, B> withSerializationUpdater(final BiFunction<NamespacedKey, ApiVersion, NamespacedKey> updater) {
+ return this;
+ }
+
@@ -479,11 +489,11 @@ index 0000000000000000000000000000000000000000..c97bda87742852c921d73f4886721f1e
+ * as fields, but instead be obtained via {@link io.papermc.paper.registry.RegistryAccess#getRegistry(RegistryKey)}
+ */
+ @Deprecated
-+ default RegistryEntry<M, B, R> delayed() {
++ default RegistryEntry<M, B> delayed() {
+ return new DelayedRegistryEntry<>(this);
+ }
+
-+ static <M, B extends Keyed> RegistryEntry<M, B, CraftRegistry<B, M>> entry(
++ static <M, B extends Keyed> RegistryEntry<M, B> entry(
+ final ResourceKey<? extends Registry<M>> mcKey,
+ final RegistryKey<B> apiKey,
+ final Class<?> classToPreload,
@@ -492,7 +502,7 @@ index 0000000000000000000000000000000000000000..c97bda87742852c921d73f4886721f1e
+ return new CraftRegistryEntry<>(mcKey, apiKey, classToPreload, minecraftToBukkit);
+ }
+
-+ static <M, B extends Keyed> RegistryEntry<M, B, org.bukkit.Registry<B>> apiOnly(
++ static <M, B extends Keyed> RegistryEntry<M, B> apiOnly(
+ final ResourceKey<? extends Registry<M>> mcKey,
+ final RegistryKey<B> apiKey,
+ final Supplier<org.bukkit.Registry<B>> apiRegistrySupplier
@@ -592,7 +602,7 @@ index 0000000000000000000000000000000000000000..5562e8da5ebaef2a3add46e88d64358b
+}
diff --git a/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistryEntry.java b/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistryEntry.java
new file mode 100644
-index 0000000000000000000000000000000000000000..5f615f50ac0cdbc47cf7a39b630b653e0d30cdf5
+index 0000000000000000000000000000000000000000..110b8d559f49f9e4f181b47663962a139a273a72
--- /dev/null
+++ b/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistryEntry.java
@@ -0,0 +1,26 @@
@@ -605,7 +615,7 @@ index 0000000000000000000000000000000000000000..5f615f50ac0cdbc47cf7a39b630b653e
+import net.minecraft.resources.ResourceKey;
+import org.bukkit.Keyed;
+
-+public record DelayedRegistryEntry<M, T extends Keyed, R extends org.bukkit.Registry<T>>(RegistryEntry<M, T, R> delegate) implements RegistryEntry<M, T, R> {
++public record DelayedRegistryEntry<M, T extends Keyed>(RegistryEntry<M, T> delegate) implements RegistryEntry<M, T> {
+
+ @Override
+ public ResourceKey<? extends Registry<M>> mcKey() {
@@ -898,10 +908,10 @@ index 0000000000000000000000000000000000000000..b9d00e65639521eecd44bd2be3e01226
+ }
+}
diff --git a/src/test/java/io/papermc/paper/registry/RegistryKeyTest.java b/src/test/java/io/papermc/paper/registry/RegistryKeyTest.java
-index e1c14886064cde56be7fcd8f22a6ecb2d222a762..f4da2afd8977030e3200ac5d4bf51b7206a90bd7 100644
+index e1c14886064cde56be7fcd8f22a6ecb2d222a762..69cece1537bb558b80e1947fdb1fe25555e82628 100644
--- a/src/test/java/io/papermc/paper/registry/RegistryKeyTest.java
+++ b/src/test/java/io/papermc/paper/registry/RegistryKeyTest.java
-@@ -1,15 +1,18 @@
+@@ -1,15 +1,19 @@
package io.papermc.paper.registry;
+import io.papermc.paper.registry.entry.RegistryEntry;
@@ -912,6 +922,7 @@ index e1c14886064cde56be7fcd8f22a6ecb2d222a762..f4da2afd8977030e3200ac5d4bf51b72
import net.minecraft.resources.ResourceLocation;
+import org.bukkit.Keyed;
import org.bukkit.support.AbstractTestingBase;
++import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -920,7 +931,7 @@ index e1c14886064cde56be7fcd8f22a6ecb2d222a762..f4da2afd8977030e3200ac5d4bf51b72
import static org.junit.jupiter.api.Assertions.assertTrue;
class RegistryKeyTest extends AbstractTestingBase {
-@@ -28,6 +31,12 @@ class RegistryKeyTest extends AbstractTestingBase {
+@@ -28,6 +32,12 @@ class RegistryKeyTest extends AbstractTestingBase {
void testApiRegistryKeysExist(final RegistryKey<?> key) {
final Optional<Registry<Object>> registry = AbstractTestingBase.REGISTRY_CUSTOM.registry(ResourceKey.createRegistryKey(ResourceLocation.parse(key.key().asString())));
assertTrue(registry.isPresent(), "Missing vanilla registry for " + key.key().asString());
@@ -929,7 +940,7 @@ index e1c14886064cde56be7fcd8f22a6ecb2d222a762..f4da2afd8977030e3200ac5d4bf51b72
+ @ParameterizedTest
+ @MethodSource("data")
+ void testRegistryEntryExists(final RegistryKey<?> key) {
-+ final RegistryEntry<?, ?, ?> entry = PaperRegistries.getEntry(key);
++ final @Nullable RegistryEntry<?, ?> entry = PaperRegistries.getEntry(key);
+ assertNotNull(entry, "Missing PaperRegistries entry for " + key);
}
}
diff --git a/patches/server/0934-Add-Lifecycle-Event-system.patch b/patches/server/0934-Add-Lifecycle-Event-system.patch
index 89149b0c71..1b988c3e25 100644
--- a/patches/server/0934-Add-Lifecycle-Event-system.patch
+++ b/patches/server/0934-Add-Lifecycle-Event-system.patch
@@ -54,7 +54,7 @@ index 30b50e6294c6eaade5e17cfaf34600d122e6251c..0bb7694188d5fb75bb756ce75d0060ea
}
diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java
new file mode 100644
-index 0000000000000000000000000000000000000000..f84c9c80e701231e5c33ac3c5573f1093e80f38b
+index 0000000000000000000000000000000000000000..d0ef7fa0b3e5935d48f894596be6672b0016948a
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java
@@ -0,0 +1,110 @@
@@ -117,15 +117,15 @@ index 0000000000000000000000000000000000000000..f84c9c80e701231e5c33ac3c5573f109
+ }
+
+ public <O extends LifecycleEventOwner, E extends PaperLifecycleEvent> void callEvent(final LifecycleEventType<O, ? super E, ?> eventType, final E event, final Predicate<? super O> ownerPredicate) {
-+ final AbstractLifecycleEventType<O, ? super E, ?, ?> lifecycleEventType = (AbstractLifecycleEventType<O, ? super E, ?, ?>) eventType;
-+ lifecycleEventType.forEachHandler(registeredHandler -> {
++ final AbstractLifecycleEventType<O, ? super E, ?> lifecycleEventType = (AbstractLifecycleEventType<O, ? super E, ?>) eventType;
++ lifecycleEventType.forEachHandler(event, registeredHandler -> {
+ try {
+ if (event instanceof final OwnerAwareLifecycleEvent<?> ownerAwareEvent) {
+ ownerAwareGenericHelper(ownerAwareEvent, registeredHandler.owner());
+ }
+ registeredHandler.lifecycleEventHandler().run(event);
+ } catch (final Throwable ex) {
-+ LOGGER.error("Could not run '{}' lifecycle event handler from {}", lifecycleEventType.name(), registeredHandler.owner().getPluginMeta().getDisplayName(), ex);
++ throw new RuntimeException("Could not run '%s' lifecycle event handler from %s".formatted(lifecycleEventType.name(), registeredHandler.owner().getPluginMeta().getDisplayName()), ex);
+ } finally {
+ if (event instanceof final OwnerAwareLifecycleEvent<?> ownerAwareEvent) {
+ ownerAwareEvent.setOwner(null);
@@ -151,7 +151,7 @@ index 0000000000000000000000000000000000000000..f84c9c80e701231e5c33ac3c5573f109
+ }
+
+ private <O extends LifecycleEventOwner> void removeEventHandlersOwnedBy(final LifecycleEventType<O, ?, ?> eventType, final Plugin possibleOwner) {
-+ final AbstractLifecycleEventType<O, ?, ?, ?> lifecycleEventType = (AbstractLifecycleEventType<O, ?, ?, ?>) eventType;
++ final AbstractLifecycleEventType<O, ?, ?> lifecycleEventType = (AbstractLifecycleEventType<O, ?, ?>) eventType;
+ lifecycleEventType.removeMatching(registeredHandler -> registeredHandler.owner().getPluginMeta().getName().equals(possibleOwner.getPluginMeta().getName()));
+ }
+
@@ -186,7 +186,7 @@ index 0000000000000000000000000000000000000000..e941405269a773e8a77e26ffd1afd84f
+}
diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEventManager.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEventManager.java
new file mode 100644
-index 0000000000000000000000000000000000000000..f1be5b9a29435bae0afd2bd951bfe88d1669e7eb
+index 0000000000000000000000000000000000000000..d05334016bd01201c755dea04c0cea56b6dfcb50
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEventManager.java
@@ -0,0 +1,26 @@
@@ -213,15 +213,15 @@ index 0000000000000000000000000000000000000000..f1be5b9a29435bae0afd2bd951bfe88d
+ @Override
+ public void registerEventHandler(final LifecycleEventHandlerConfiguration<? super O> handlerConfiguration) {
+ Preconditions.checkState(this.registrationCheck.getAsBoolean(), "Cannot register lifecycle event handlers");
-+ ((AbstractLifecycleEventHandlerConfiguration<? super O, ?, ?>) handlerConfiguration).registerFrom(this.owner);
++ ((AbstractLifecycleEventHandlerConfiguration<? super O, ?>) handlerConfiguration).registerFrom(this.owner);
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/AbstractLifecycleEventHandlerConfiguration.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/AbstractLifecycleEventHandlerConfiguration.java
new file mode 100644
-index 0000000000000000000000000000000000000000..6a85a4f581612efff04c1a955493aa2e32476277
+index 0000000000000000000000000000000000000000..fa216e6fd804859293385ed43c53dfca057f317f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/AbstractLifecycleEventHandlerConfiguration.java
-@@ -0,0 +1,26 @@
+@@ -0,0 +1,28 @@
+package io.papermc.paper.plugin.lifecycle.event.handler.configuration;
+
+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
@@ -232,28 +232,30 @@ index 0000000000000000000000000000000000000000..6a85a4f581612efff04c1a955493aa2e
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+@DefaultQualifier(NonNull.class)
-+public abstract class AbstractLifecycleEventHandlerConfiguration<O extends LifecycleEventOwner, E extends LifecycleEvent, CI extends AbstractLifecycleEventHandlerConfiguration<O, E, CI>> implements LifecycleEventHandlerConfiguration<O> {
++public abstract class AbstractLifecycleEventHandlerConfiguration<O extends LifecycleEventOwner, E extends LifecycleEvent> implements LifecycleEventHandlerConfiguration<O> {
+
+ private final LifecycleEventHandler<? super E> handler;
-+ private final AbstractLifecycleEventType<O, E, ?, CI> type;
++ private final AbstractLifecycleEventType<O, E, ?> type;
+
-+ protected AbstractLifecycleEventHandlerConfiguration(final LifecycleEventHandler<? super E> handler, final AbstractLifecycleEventType<O, E, ?, CI> type) {
++ protected AbstractLifecycleEventHandlerConfiguration(final LifecycleEventHandler<? super E> handler, final AbstractLifecycleEventType<O, E, ?> type) {
+ this.handler = handler;
+ this.type = type;
+ }
+
-+ public abstract CI config();
-+
+ public final void registerFrom(final O owner) {
-+ this.type.tryRegister(owner, this.handler, this.config());
++ this.type.tryRegister(owner, this);
++ }
++
++ public LifecycleEventHandler<? super E> handler() {
++ return this.handler;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/MonitorLifecycleEventHandlerConfigurationImpl.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/MonitorLifecycleEventHandlerConfigurationImpl.java
new file mode 100644
-index 0000000000000000000000000000000000000000..e0699fcd0a098abc5e1206e7c0fa80b96eca7884
+index 0000000000000000000000000000000000000000..ab444d60d72bd692843052df5d7b24fbb5621cf7
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/MonitorLifecycleEventHandlerConfigurationImpl.java
-@@ -0,0 +1,33 @@
+@@ -0,0 +1,28 @@
+package io.papermc.paper.plugin.lifecycle.event.handler.configuration;
+
+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
@@ -264,19 +266,14 @@ index 0000000000000000000000000000000000000000..e0699fcd0a098abc5e1206e7c0fa80b9
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+@DefaultQualifier(NonNull.class)
-+public class MonitorLifecycleEventHandlerConfigurationImpl<O extends LifecycleEventOwner, E extends LifecycleEvent> extends AbstractLifecycleEventHandlerConfiguration<O, E, MonitorLifecycleEventHandlerConfigurationImpl<O, E>> implements MonitorLifecycleEventHandlerConfiguration<O> {
++public class MonitorLifecycleEventHandlerConfigurationImpl<O extends LifecycleEventOwner, E extends LifecycleEvent> extends AbstractLifecycleEventHandlerConfiguration<O, E> implements MonitorLifecycleEventHandlerConfiguration<O> {
+
+ private boolean monitor = false;
+
-+ public MonitorLifecycleEventHandlerConfigurationImpl(final LifecycleEventHandler<? super E> handler, final AbstractLifecycleEventType<O, E, ?, MonitorLifecycleEventHandlerConfigurationImpl<O, E>> eventType) {
++ public MonitorLifecycleEventHandlerConfigurationImpl(final LifecycleEventHandler<? super E> handler, final AbstractLifecycleEventType<O, E, ?> eventType) {
+ super(handler, eventType);
+ }
+
-+ @Override
-+ public MonitorLifecycleEventHandlerConfigurationImpl<O, E> config() {
-+ return this;
-+ }
-+
+ public boolean isMonitor() {
+ return this.monitor;
+ }
@@ -289,10 +286,10 @@ index 0000000000000000000000000000000000000000..e0699fcd0a098abc5e1206e7c0fa80b9
+}
diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/PrioritizedLifecycleEventHandlerConfigurationImpl.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/PrioritizedLifecycleEventHandlerConfigurationImpl.java
new file mode 100644
-index 0000000000000000000000000000000000000000..c1d0070fc1594f7a7c29d7dc679da7b347a7140b
+index 0000000000000000000000000000000000000000..ccdad31717bf12b844cbeaf11a49247485ec77f1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/PrioritizedLifecycleEventHandlerConfigurationImpl.java
-@@ -0,0 +1,43 @@
+@@ -0,0 +1,40 @@
+package io.papermc.paper.plugin.lifecycle.event.handler.configuration;
+
+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
@@ -304,22 +301,19 @@ index 0000000000000000000000000000000000000000..c1d0070fc1594f7a7c29d7dc679da7b3
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+@DefaultQualifier(NonNull.class)
-+public class PrioritizedLifecycleEventHandlerConfigurationImpl<O extends LifecycleEventOwner, E extends LifecycleEvent> extends AbstractLifecycleEventHandlerConfiguration<O, E, PrioritizedLifecycleEventHandlerConfigurationImpl<O, E>> implements PrioritizedLifecycleEventHandlerConfiguration<O> {
++public class PrioritizedLifecycleEventHandlerConfigurationImpl<O extends LifecycleEventOwner, E extends LifecycleEvent>
++ extends AbstractLifecycleEventHandlerConfiguration<O, E>
++ implements PrioritizedLifecycleEventHandlerConfiguration<O> {
+
+ private static final OptionalInt DEFAULT_PRIORITY = OptionalInt.of(0);
+ private static final OptionalInt MONITOR_PRIORITY = OptionalInt.empty();
+
+ private OptionalInt priority = DEFAULT_PRIORITY;
+
-+ public PrioritizedLifecycleEventHandlerConfigurationImpl(final LifecycleEventHandler<? super E> handler, final AbstractLifecycleEventType<O, E, ?, PrioritizedLifecycleEventHandlerConfigurationImpl<O, E>> eventType) {
++ public PrioritizedLifecycleEventHandlerConfigurationImpl(final LifecycleEventHandler<? super E> handler, final AbstractLifecycleEventType<O, E, ?> eventType) {
+ super(handler, eventType);
+ }
+
-+ @Override
-+ public PrioritizedLifecycleEventHandlerConfigurationImpl<O, E> config() {
-+ return this;
-+ }
-+
+ public OptionalInt priority() {
+ return this.priority;
+ }
@@ -435,10 +429,10 @@ index 0000000000000000000000000000000000000000..6d530c52aaf0dc2cdfe3bd56af557274
+}
diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/AbstractLifecycleEventType.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/AbstractLifecycleEventType.java
new file mode 100644
-index 0000000000000000000000000000000000000000..a65fb37f4a729e2fe9fb81af822db626ec7e6d7b
+index 0000000000000000000000000000000000000000..9359a36d26970742da3a7abb0050158cd6c64e8e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/AbstractLifecycleEventType.java
-@@ -0,0 +1,50 @@
+@@ -0,0 +1,54 @@
+package io.papermc.paper.plugin.lifecycle.event.types;
+
+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
@@ -453,7 +447,7 @@ index 0000000000000000000000000000000000000000..a65fb37f4a729e2fe9fb81af822db626
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+@DefaultQualifier(NonNull.class)
-+public abstract class AbstractLifecycleEventType<O extends LifecycleEventOwner, E extends LifecycleEvent, C extends LifecycleEventHandlerConfiguration<O>, CI extends AbstractLifecycleEventHandlerConfiguration<O, E, CI>> implements LifecycleEventType<O, E, C> {
++public abstract class AbstractLifecycleEventType<O extends LifecycleEventOwner, E extends LifecycleEvent, C extends LifecycleEventHandlerConfiguration<O>> implements LifecycleEventType<O, E, C> {
+
+ private final String name;
+ private final Class<? extends O> ownerType;
@@ -474,24 +468,28 @@ index 0000000000000000000000000000000000000000..a65fb37f4a729e2fe9fb81af822db626
+ }
+ }
+
-+ public abstract void forEachHandler(Consumer<? super RegisteredHandler<O, E>> consumer, Predicate<? super RegisteredHandler<O, E>> predicate);
++ public abstract void forEachHandler(E event, Consumer<RegisteredHandler<O, E>> consumer, Predicate<RegisteredHandler<O, E>> predicate);
+
-+ public abstract void removeMatching(Predicate<? super RegisteredHandler<O, E>> predicate);
++ public abstract void removeMatching(Predicate<RegisteredHandler<O, E>> predicate);
+
-+ protected abstract void register(O owner, LifecycleEventHandler<? super E> handler, CI config);
++ protected abstract void register(O owner, AbstractLifecycleEventHandlerConfiguration<O, E> config);
+
-+ public final void tryRegister(final O owner, final LifecycleEventHandler<? super E> handler, final CI config) {
++ public final void tryRegister(final O owner, final AbstractLifecycleEventHandlerConfiguration<O, E> config) {
+ this.verifyOwner(owner);
+ LifecycleEventRunner.INSTANCE.checkRegisteredHandler(owner, this);
-+ this.register(owner, handler, config);
++ this.register(owner, config);
+ }
+
-+ public record RegisteredHandler<O, E extends LifecycleEvent>(O owner, LifecycleEventHandler<? super E> lifecycleEventHandler) {
++ public record RegisteredHandler<O extends LifecycleEventOwner, E extends LifecycleEvent>(O owner, AbstractLifecycleEventHandlerConfiguration<O, E> config) {
++
++ public LifecycleEventHandler<? super E> lifecycleEventHandler() {
++ return this.config().handler();
++ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEventTypeProviderImpl.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEventTypeProviderImpl.java
new file mode 100644
-index 0000000000000000000000000000000000000000..0886edad92b40276f268bd745b31bac359fd28af
+index 0000000000000000000000000000000000000000..af0cb3298d9c737417c6e54b360f8dc50a5caf04
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEventTypeProviderImpl.java
@@ -0,0 +1,25 @@
@@ -517,20 +515,21 @@ index 0000000000000000000000000000000000000000..0886edad92b40276f268bd745b31bac3
+
+ @Override
+ public <O extends LifecycleEventOwner, E extends LifecycleEvent> LifecycleEventType.Prioritizable<O, E> prioritized(final String name, final Class<? extends O> ownerType) {
-+ return LifecycleEventRunner.INSTANCE.addEventType(new PrioritizableLifecycleEventType<>(name, ownerType));
++ return LifecycleEventRunner.INSTANCE.addEventType(new PrioritizableLifecycleEventType.Simple<>(name, ownerType));
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/MonitorableLifecycleEventType.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/MonitorableLifecycleEventType.java
new file mode 100644
-index 0000000000000000000000000000000000000000..6d92c1d3adf220154dfe7cba3a3f8158356c3e3c
+index 0000000000000000000000000000000000000000..c71912f0050ce0cc6e416948a354c8a66da606a8
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/MonitorableLifecycleEventType.java
-@@ -0,0 +1,54 @@
+@@ -0,0 +1,58 @@
+package io.papermc.paper.plugin.lifecycle.event.types;
+
+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner;
+import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler;
++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.AbstractLifecycleEventHandlerConfiguration;
+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.MonitorLifecycleEventHandlerConfiguration;
+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.MonitorLifecycleEventHandlerConfigurationImpl;
+import java.util.ArrayList;
@@ -541,7 +540,7 @@ index 0000000000000000000000000000000000000000..6d92c1d3adf220154dfe7cba3a3f8158
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+@DefaultQualifier(NonNull.class)
-+public class MonitorableLifecycleEventType<O extends LifecycleEventOwner, E extends LifecycleEvent> extends AbstractLifecycleEventType<O, E, MonitorLifecycleEventHandlerConfiguration<O>, MonitorLifecycleEventHandlerConfigurationImpl<O, E>> implements LifecycleEventType.Monitorable<O, E> {
++public class MonitorableLifecycleEventType<O extends LifecycleEventOwner, E extends LifecycleEvent> extends AbstractLifecycleEventType<O, E, MonitorLifecycleEventHandlerConfiguration<O>> implements LifecycleEventType.Monitorable<O, E> {
+
+ final List<RegisteredHandler<O, E>> handlers = new ArrayList<>();
+ int nonMonitorIdx = 0;
@@ -556,9 +555,12 @@ index 0000000000000000000000000000000000000000..6d92c1d3adf220154dfe7cba3a3f8158
+ }
+
+ @Override
-+ protected void register(final O owner, final LifecycleEventHandler<? super E> handler, final MonitorLifecycleEventHandlerConfigurationImpl<O, E> config) {
-+ final RegisteredHandler<O, E> registeredHandler = new RegisteredHandler<>(owner, handler);
-+ if (!config.isMonitor()) {
++ protected void register(final O owner, final AbstractLifecycleEventHandlerConfiguration<O, E> config) {
++ if (!(config instanceof final MonitorLifecycleEventHandlerConfigurationImpl<?,?> monitor)) {
++ throw new IllegalArgumentException("Configuration must be a MonitorLifecycleEventHandlerConfiguration");
++ }
++ final RegisteredHandler<O, E> registeredHandler = new RegisteredHandler<>(owner, config);
++ if (!monitor.isMonitor()) {
+ this.handlers.add(this.nonMonitorIdx, registeredHandler);
+ this.nonMonitorIdx++;
+ } else {
@@ -567,7 +569,7 @@ index 0000000000000000000000000000000000000000..6d92c1d3adf220154dfe7cba3a3f8158
+ }
+
+ @Override
-+ public void forEachHandler(final Consumer<? super RegisteredHandler<O, E>> consumer, final Predicate<? super RegisteredHandler<O, E>> predicate) {
++ public void forEachHandler(final E event, final Consumer<RegisteredHandler<O, E>> consumer, final Predicate<RegisteredHandler<O, E>> predicate) {
+ for (final RegisteredHandler<O, E> handler : this.handlers) {
+ if (predicate.test(handler)) {
+ consumer.accept(handler);
@@ -576,7 +578,7 @@ index 0000000000000000000000000000000000000000..6d92c1d3adf220154dfe7cba3a3f8158
+ }
+
+ @Override
-+ public void removeMatching(final Predicate<? super RegisteredHandler<O, E>> predicate) {
++ public void removeMatching(final Predicate<RegisteredHandler<O, E>> predicate) {
+ this.handlers.removeIf(predicate);
+ }
+}
@@ -603,30 +605,35 @@ index 0000000000000000000000000000000000000000..3e7e7474f301c0725fa2bcd6e19e476f
+}
diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/PrioritizableLifecycleEventType.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/PrioritizableLifecycleEventType.java
new file mode 100644
-index 0000000000000000000000000000000000000000..6629f7fabf66ce761024268043cc30076ba8a3f1
+index 0000000000000000000000000000000000000000..76f92a6fc84c0315f3973dc4e92649b66babc3d5
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/PrioritizableLifecycleEventType.java
-@@ -0,0 +1,64 @@
+@@ -0,0 +1,74 @@
+package io.papermc.paper.plugin.lifecycle.event.types;
+
++import com.google.common.base.Preconditions;
+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner;
+import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler;
++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.AbstractLifecycleEventHandlerConfiguration;
+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.PrioritizedLifecycleEventHandlerConfiguration;
+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.PrioritizedLifecycleEventHandlerConfigurationImpl;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
-+import java.util.OptionalInt;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+@DefaultQualifier(NonNull.class)
-+public class PrioritizableLifecycleEventType<O extends LifecycleEventOwner, E extends LifecycleEvent> extends AbstractLifecycleEventType<O, E, PrioritizedLifecycleEventHandlerConfiguration<O>, PrioritizedLifecycleEventHandlerConfigurationImpl<O, E>> implements LifecycleEventType.Prioritizable<O, E> {
++public abstract class PrioritizableLifecycleEventType<
++ O extends LifecycleEventOwner,
++ E extends LifecycleEvent,
++ C extends PrioritizedLifecycleEventHandlerConfiguration<O>
++> extends AbstractLifecycleEventType<O, E, C> {
+
-+ private static final Comparator<PrioritizedHandler<?, ?>> COMPARATOR = Comparator.comparing(PrioritizedHandler::priority, (o1, o2) -> {
++ private static final Comparator<RegisteredHandler<?, ?>> COMPARATOR = Comparator.comparing(handler -> ((PrioritizedLifecycleEventHandlerConfigurationImpl<?, ?>) handler.config()).priority(), (o1, o2) -> {
+ if (o1.equals(o2)) {
+ return 0;
+ } else if (o1.isEmpty()) {
@@ -638,38 +645,43 @@ index 0000000000000000000000000000000000000000..6629f7fabf66ce761024268043cc3007
+ }
+ });
+
-+ private final List<PrioritizedHandler<O, E>> handlers = new ArrayList<>();
++ private final List<RegisteredHandler<O, E>> handlers = new ArrayList<>();
+
+ public PrioritizableLifecycleEventType(final String name, final Class<? extends O> ownerType) {
+ super(name, ownerType);
+ }
+
+ @Override
-+ public PrioritizedLifecycleEventHandlerConfiguration<O> newHandler(final LifecycleEventHandler<? super E> handler) {
-+ return new PrioritizedLifecycleEventHandlerConfigurationImpl<>(handler, this);
-+ }
-+
-+ @Override
-+ protected void register(final O owner, final LifecycleEventHandler<? super E> handler, final PrioritizedLifecycleEventHandlerConfigurationImpl<O, E> config) {
-+ this.handlers.add(new PrioritizedHandler<>(new RegisteredHandler<>(owner, handler), config.priority()));
++ protected void register(final O owner, final AbstractLifecycleEventHandlerConfiguration<O, E> config) {
++ Preconditions.checkArgument(config instanceof PrioritizedLifecycleEventHandlerConfigurationImpl<?, ?>, "Configuration must be a PrioritizedLifecycleEventHandlerConfiguration");
++ this.handlers.add(new RegisteredHandler<>(owner, config));
+ this.handlers.sort(COMPARATOR);
+ }
+
+ @Override
-+ public void forEachHandler(final Consumer<? super RegisteredHandler<O, E>> consumer, final Predicate<? super RegisteredHandler<O, E>> predicate) {
-+ for (final PrioritizedHandler<O, E> handler : this.handlers) {
-+ if (predicate.test(handler.handler())) {
-+ consumer.accept(handler.handler());
++ public void forEachHandler(final E event, final Consumer<RegisteredHandler<O, E>> consumer, final Predicate<RegisteredHandler<O, E>> predicate) {
++ for (final RegisteredHandler<O, E> handler : this.handlers) {
++ if (predicate.test(handler)) {
++ consumer.accept(handler);
+ }
+ }
+ }
+
+ @Override
-+ public void removeMatching(final Predicate<? super RegisteredHandler<O, E>> predicate) {
-+ this.handlers.removeIf(prioritizedHandler -> predicate.test(prioritizedHandler.handler()));
++ public void removeMatching(final Predicate<RegisteredHandler<O, E>> predicate) {
++ this.handlers.removeIf(predicate);
+ }
+
-+ private record PrioritizedHandler<O extends LifecycleEventOwner, E extends LifecycleEvent>(RegisteredHandler<O, E> handler, OptionalInt priority) {}
++ public static class Simple<O extends LifecycleEventOwner, E extends LifecycleEvent> extends PrioritizableLifecycleEventType<O, E, PrioritizedLifecycleEventHandlerConfiguration<O>> implements LifecycleEventType.Prioritizable<O, E> {
++ public Simple(final String name, final Class<? extends O> ownerType) {
++ super(name, ownerType);
++ }
++
++ @Override
++ public PrioritizedLifecycleEventHandlerConfiguration<O> newHandler(final LifecycleEventHandler<? super E> handler) {
++ return new PrioritizedLifecycleEventHandlerConfigurationImpl<>(handler, this);
++ }
++ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
index 834b85f24df023642f8abf7213fe578ac8c17a3e..3e82ea07ca4194844c5528446e2c4a46ff4acee5 100644
diff --git a/patches/server/0975-Brigadier-based-command-API.patch b/patches/server/0975-Brigadier-based-command-API.patch
index 96d91304d7..51f1f66e5f 100644
--- a/patches/server/0975-Brigadier-based-command-API.patch
+++ b/patches/server/0975-Brigadier-based-command-API.patch
@@ -1070,7 +1070,7 @@ index 0000000000000000000000000000000000000000..72966584089d3fee9778f572727c9b7f
+}
diff --git a/src/main/java/io/papermc/paper/command/brigadier/argument/VanillaArgumentProviderImpl.java b/src/main/java/io/papermc/paper/command/brigadier/argument/VanillaArgumentProviderImpl.java
new file mode 100644
-index 0000000000000000000000000000000000000000..93edb22c8500e79f86b101ef38955bca45a8d3a9
+index 0000000000000000000000000000000000000000..1b389cd0e77c24874b2a825608b612e3fc4f3dd6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/command/brigadier/argument/VanillaArgumentProviderImpl.java
@@ -0,0 +1,354 @@
@@ -1361,7 +1361,7 @@ index 0000000000000000000000000000000000000000..93edb22c8500e79f86b101ef38955bca
+ @Override
+ public <T> ArgumentType<TypedKey<T>> resourceKey(final RegistryKey<T> registryKey) {
+ return this.wrap(
-+ ResourceKeyArgument.key(PaperRegistries.toNms(registryKey)),
++ ResourceKeyArgument.key(PaperRegistries.registryToNms(registryKey)),
+ nmsRegistryKey -> TypedKey.create(registryKey, CraftNamespacedKey.fromMinecraft(nmsRegistryKey.location()))
+ );
+ }
@@ -1375,7 +1375,7 @@ index 0000000000000000000000000000000000000000..93edb22c8500e79f86b101ef38955bca
+ private <T, K extends Keyed> ArgumentType<T> resourceRaw(final RegistryKey registryKeyRaw) { // TODO remove Keyed
+ final RegistryKey<K> registryKey = registryKeyRaw;
+ return (ArgumentType<T>) this.wrap(
-+ ResourceArgument.resource(PaperCommands.INSTANCE.getBuildContext(), PaperRegistries.toNms(registryKey)),
++ ResourceArgument.resource(PaperCommands.INSTANCE.getBuildContext(), PaperRegistries.registryToNms(registryKey)),
+ resource -> requireNonNull(
+ RegistryAccess.registryAccess()
+ .getRegistry(registryKey)
@@ -1982,7 +1982,7 @@ index 0000000000000000000000000000000000000000..0c3c82b28e581286b798ee58ca4193ef
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java
-index f84c9c80e701231e5c33ac3c5573f1093e80f38b..6c072e44a8144de6658b4eb818c996f0eac5805b 100644
+index d0ef7fa0b3e5935d48f894596be6672b0016948a..cca76f2d1623952017a83fdb027f77a601c79b3e 100644
--- a/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java
+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java
@@ -9,6 +9,7 @@ import io.papermc.paper.plugin.lifecycle.event.registrar.RegistrarEventImpl;
diff --git a/patches/server/1021-Registry-Modification-API.patch b/patches/server/1021-Registry-Modification-API.patch
new file mode 100644
index 0000000000..d8e13337bd
--- /dev/null
+++ b/patches/server/1021-Registry-Modification-API.patch
@@ -0,0 +1,1427 @@
+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 1e098dc25bd338ff179491ff3382ac56aad9948e..a688af29273ebfbb4f75dd74cd30627fc481c96c 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;
+@@ -46,6 +47,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 {
+@@ -128,6 +130,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 d591e3a2e19d5358a0d25a5a681368943622d231..f05ebf453406a924da3de6fb250f4793a1b3c612 100644
+--- a/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java
++++ b/src/main/java/io/papermc/paper/registry/PaperRegistryAccess.java
+@@ -80,6 +80,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..6932ffef54f90cc486f517561f73c2e4daf3983b
+--- /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(@Nullable 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(@Nullable 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..a7f2b264d4f37f5293ae72195b4c78faf35351c9
+--- /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 final class PaperRegistryListenerManager {
++
++ public static final PaperRegistryListenerManager INSTANCE = new PaperRegistryListenerManager();
++
++ public final RegistryEventMap valueAddHooks = new RegistryEventMap("value add");
++ public final RegistryEventMap freezeHooks = 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.valueAddHooks.hasHooks(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.valueAddHooks.hasHooks(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.valueAddHooks.getHook(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.freezeHooks.hasHooks(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.freezeHooks.getHook(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.valueAddHooks.getOrCreate(type, 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.freezeHooks.getOrCreate(type, 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..a93ee8a52c6ec8b8e9712e8449a9c0e6c3fd4046
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/entry/AddableRegistryEntry.java
+@@ -0,0 +1,46 @@
++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.RegistryOps;
++import net.minecraft.resources.ResourceKey;
++import org.bukkit.Keyed;
++import org.bukkit.NamespacedKey;
++import org.checkerframework.checker.nullness.qual.Nullable;
++
++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 @Nullable 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..cdc490c5254a80f8c38ff0b56c0fcd973df87f59
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/entry/ModifiableRegistryEntry.java
+@@ -0,0 +1,33 @@
++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;
++import org.checkerframework.checker.nullness.qual.Nullable;
++
++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 @Nullable 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..4712ff8be61994f4b0fa7c14a688c828ef13a175 100644
+--- a/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java
++++ b/src/main/java/io/papermc/paper/registry/entry/RegistryEntry.java
+@@ -1,16 +1,24 @@
+ 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;
+ import net.minecraft.core.Registry;
++import net.minecraft.resources.RegistryOps;
+ import net.minecraft.resources.ResourceKey;
+ 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 +40,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(@Nullable 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 +115,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..f5ea23173dcbe491742c3dd051c147ef397307a0
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/RegistryEventMap.java
+@@ -0,0 +1,44 @@
++package io.papermc.paper.registry.event;
++
++import io.papermc.paper.plugin.bootstrap.BootstrapContext;
++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner;
++import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType;
++import io.papermc.paper.registry.RegistryBuilder;
++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 RegistryEvent<?>, ?>> hooks = new HashMap<>();
++ private final String name;
++
++ public RegistryEventMap(final String name) {
++ this.name = name;
++ }
++
++ @SuppressWarnings("unchecked")
++ public <T, B extends RegistryBuilder<T>, E extends RegistryEvent<T>, ET extends LifecycleEventType<BootstrapContext, E, ?>> ET getOrCreate(final RegistryEventProvider<T, B> type, final BiFunction<? super RegistryEventProvider<T, B>, ? super String, ET> eventTypeCreator) {
++ final ET registerHook;
++ if (this.hooks.containsKey(type.registryKey())) {
++ registerHook = (ET) this.hooks.get(type.registryKey());
++ } else {
++ registerHook = eventTypeCreator.apply(type, this.name);
++ LifecycleEventRunner.INSTANCE.addEventType(registerHook);
++ this.hooks.put(type.registryKey(), registerHook);
++ }
++ return registerHook;
++ }
++
++ @SuppressWarnings("unchecked")
++ public <T, E extends RegistryEvent<T>> LifecycleEventType<BootstrapContext, E, ?> getHook(final RegistryKey<T> registryKey) {
++ return (LifecycleEventType<BootstrapContext, E, ?>) Objects.requireNonNull(this.hooks.get(registryKey), "No hook for " + registryKey);
++ }
++
++ public boolean hasHooks(final RegistryKey<?> registryKey) {
++ return this.hooks.containsKey(registryKey);
++ }
++
++}
+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..5d709ed04e1078b631f5b9c74ca35f042251e14f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryEntryAddEventTypeImpl.java
+@@ -0,0 +1,32 @@
++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.event.RegistryEntryAddEvent;
++import io.papermc.paper.registry.event.RegistryEventProvider;
++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 RegistryEventProvider<T, B> type, final String eventName) {
++ super(type.registryKey() + " / " + eventName, BootstrapContext.class);
++ }
++
++ @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..dcc0f6b337840a78d38abdf2eb3f4bbd1676f58f
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/registry/event/type/RegistryLifecycleEventType.java
+@@ -0,0 +1,14 @@
++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.RegistryBuilder;
++import io.papermc.paper.registry.event.RegistryEvent;
++import io.papermc.paper.registry.event.RegistryEventProvider;
++
++public final class RegistryLifecycleEventType<T, B extends RegistryBuilder<T>, E extends RegistryEvent<T>> extends PrioritizableLifecycleEventType.Simple<BootstrapContext, E> {
++
++ public RegistryLifecycleEventType(final RegistryEventProvider<T, B> type, final String eventName) {
++ super(type.registryKey() + " / " + eventName, BootstrapContext.class);
++ }
++}
+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 5562e8da5ebaef2a3add46e88d64358b7737b59e..e5880f76cdb8ebf01fcefdf77ba9b95674b997a8 100644
+--- a/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java
++++ b/src/main/java/io/papermc/paper/registry/legacy/DelayedRegistry.java
+@@ -1,12 +1,13 @@
+ 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;
+ 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;
+@@ -52,4 +53,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 edbbafd1705345282e5e6251eb71bfde5793b7d4..f22d22ebcedcc9c20225677844c86a1ad27c4211 100644
+--- a/src/main/java/net/minecraft/core/MappedRegistry.java
++++ b/src/main/java/net/minecraft/core/MappedRegistry.java
+@@ -441,4 +441,12 @@ public class MappedRegistry<T> implements WritableRegistry<T> {
+ public HolderLookup.RegistryLookup<T> asLookup() {
+ return this.lookup;
+ }
++ // 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 44b7927081b476813505cab6b3a2da2ec2942c54..0497318e8f647453f38f3a16a8be6bd9aa19253f 100644
+--- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
++++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
+@@ -288,6 +288,17 @@ public class BuiltInRegistries {
+ Registries.ENCHANTMENT_PROVIDER_TYPE, EnchantmentProviderTypes::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);
+@@ -328,6 +339,7 @@ public class BuiltInRegistries {
+ }
+ public static void bootStrap(Runnable runnable) {
+ // Paper end
++ REGISTRY.freeze(); // Paper - freeze main registry early
+ createContents();
+ runnable.run(); // Paper
+ freeze();
+@@ -346,6 +358,7 @@ public class BuiltInRegistries {
+ REGISTRY.freeze();
+
+ for (Registry<?> registry : 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 abadf4abe08dc3bb6612b42cbb3f7df3ffa28ce9..3053243866c655829fe2e980446b4abf1da6d37c 100644
+--- a/src/main/java/net/minecraft/resources/RegistryDataLoader.java
++++ b/src/main/java/net/minecraft/resources/RegistryDataLoader.java
+@@ -115,7 +115,7 @@ public class RegistryDataLoader {
+ );
+
+ public static RegistryAccess.Frozen load(ResourceManager resourceManager, RegistryAccess registryManager, List<RegistryDataLoader.RegistryData<?>> entries) {
+- return load((loader, infoGetter) -> loader.loadFromResources(resourceManager, infoGetter), registryManager, entries);
++ return load((loader, infoGetter, conversions) -> loader.loadFromResources(resourceManager, infoGetter, conversions), registryManager, entries); // Paper - pass conversions
+ }
+
+ public static RegistryAccess.Frozen load(
+@@ -124,7 +124,7 @@ public class RegistryDataLoader {
+ RegistryAccess registryManager,
+ List<RegistryDataLoader.RegistryData<?>> entries
+ ) {
+- return load((loader, infoGetter) -> loader.loadFromNetwork(data, factory, infoGetter), registryManager, entries);
++ return load((loader, infoGetter, conversions) -> loader.loadFromNetwork(data, factory, infoGetter, conversions), registryManager, entries); // Paper - pass conversions
+ }
+
+ private static RegistryAccess.Frozen load(
+@@ -133,9 +133,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(baseRegistryManager, 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();
+@@ -193,13 +195,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
+ }
+ }
+
+@@ -208,7 +210,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);
+@@ -221,7 +224,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,
+@@ -237,7 +240,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
+ ) {
+ List<RegistrySynchronization.PackedRegistryEntry> list = data.get(registry.key());
+ if (list != null) {
+@@ -264,7 +268,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));
+ }
+@@ -274,22 +278,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<?>>, List<RegistrySynchronization.PackedRegistryEntry>> 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 RegistryData<T>(ResourceKey<? extends Registry<T>> key, Codec<T> elementCodec, boolean requiredNonEmpty) {
+diff --git a/src/main/java/net/minecraft/server/ReloadableServerRegistries.java b/src/main/java/net/minecraft/server/ReloadableServerRegistries.java
+index 397bdacab9517354875ebc0bc68d35059b3c318b..908431652a0fea79b5a0cee1efd0c7a7d524b614 100644
+--- a/src/main/java/net/minecraft/server/ReloadableServerRegistries.java
++++ b/src/main/java/net/minecraft/server/ReloadableServerRegistries.java
+@@ -47,15 +47,16 @@ public class ReloadableServerRegistries {
+ ) {
+ RegistryAccess.Frozen frozen = dynamicRegistries.getAccessForLoading(RegistryLayer.RELOADABLE);
+ RegistryOps<JsonElement> registryOps = new ReloadableServerRegistries.EmptyTagLookupWrapper(frozen).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<?>>> list = LootDataType.values()
+- .map(type -> scheduleElementParse((LootDataType<?>)type, registryOps, resourceManager, prepareExecutor))
++ .map(type -> scheduleElementParse((LootDataType<?>)type, registryOps, resourceManager, prepareExecutor, conversions)) // Paper
+ .toList();
+ CompletableFuture<List<WritableRegistry<?>>> completableFuture = Util.sequence(list);
+ return completableFuture.thenApplyAsync(registries -> apply(dynamicRegistries, (List<WritableRegistry<?>>)registries), prepareExecutor);
+ }
+
+ private static <T> CompletableFuture<WritableRegistry<?>> scheduleElementParse(
+- 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(
+ () -> {
+@@ -66,7 +67,7 @@ public class ReloadableServerRegistries {
+ SimpleJsonResourceReloadListener.scanDirectory(resourceManager, string, GSON, map);
+ map.forEach(
+ (id, json) -> type.deserialize(id, ops, json)
+- .ifPresent(value -> writableRegistry.register(ResourceKey.create(type.registryKey(), id), (T)value, DEFAULT_REGISTRATION_INFO))
++ .ifPresent(value -> io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.registerWithListeners(writableRegistry, ResourceKey.create(type.registryKey(), id), value, DEFAULT_REGISTRATION_INFO, conversions)) // Paper - register with listeners
+ );
+ return writableRegistry;
+ },
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java
+index d21b7e39d71c785f47f790e1ad4be33a8e8e6e51..a47421425a8d5d2f07e08890fded0f7bfec4efb7 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java
+@@ -156,11 +156,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;
+@@ -233,4 +233,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 5a04134973dd1db7f778a57ec5f185feec370990..8dba6c4a2e1f305cf576e8bfdca5d0c07ab871ae 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -685,6 +685,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..f27e5e0037b719b1fc10703f8d298d2326b00432
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/registry/RegistryBuilderTest.java
+@@ -0,0 +1,34 @@
++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.AbstractTestingBase;
++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;
++
++class RegistryBuilderTest extends AbstractTestingBase {
++
++ 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 = AbstractTestingBase.REGISTRY_CUSTOM.registryOrThrow(resourceKey);
++ for (final Map.Entry<ResourceKey<M>, M> entry : registry.entrySet()) {
++ final M built = filler.fill(new Conversions(new RegistryOps.HolderLookupAdapter(AbstractTestingBase.REGISTRY_CUSTOM)), PaperRegistries.fromNms(entry.getKey()), entry.getValue()).build();
++ assertEquals(entry.getValue(), built);
++ }
++ }
++}