From b71ad2ebb344b0b468638b6311aea55bf061f518 Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Mon, 23 Dec 2024 18:47:30 -0800 Subject: [PATCH] Add datapack registration lifecycle event --- build-data/paper.at | 2 + .../io/papermc/paper/datapack/Datapack.java | 63 +----- .../paper/datapack/DatapackRegistrar.java | 204 ++++++++++++++++++ .../paper/datapack/DiscoveredDatapack.java | 69 ++++++ .../event/types/LifecycleEvents.java | 10 + .../server/MinecraftServer.java.patch | 9 + .../server/commands/ReloadCommand.java.patch | 9 + .../repository/PackRepository.java.patch | 75 +++++++ .../repository/ServerPacksSource.java.patch | 12 +- .../papermc/paper/datapack/PaperDatapack.java | 61 +----- .../datapack/PaperDatapackRegistrar.java | 165 ++++++++++++++ .../datapack/PaperDiscoveredDatapack.java | 68 ++++++ .../paper/datapack/PluginPackSource.java | 25 +++ .../testplugin/TestPluginBootstrap.java | 17 ++ .../machine_maker/tags/entity_type/test.json | 6 + .../data/machine_maker/tags/item/test.json | 6 + .../src/main/resources/pack/tags/pack.mcmeta | 6 + 17 files changed, 697 insertions(+), 110 deletions(-) create mode 100644 paper-api/src/main/java/io/papermc/paper/datapack/DatapackRegistrar.java create mode 100644 paper-api/src/main/java/io/papermc/paper/datapack/DiscoveredDatapack.java create mode 100644 paper-server/patches/sources/net/minecraft/server/packs/repository/PackRepository.java.patch create mode 100644 paper-server/src/main/java/io/papermc/paper/datapack/PaperDatapackRegistrar.java create mode 100644 paper-server/src/main/java/io/papermc/paper/datapack/PaperDiscoveredDatapack.java create mode 100644 paper-server/src/main/java/io/papermc/paper/datapack/PluginPackSource.java create mode 100644 test-plugin/src/main/resources/pack/data/machine_maker/tags/entity_type/test.json create mode 100644 test-plugin/src/main/resources/pack/data/machine_maker/tags/item/test.json create mode 100644 test-plugin/src/main/resources/pack/tags/pack.mcmeta diff --git a/build-data/paper.at b/build-data/paper.at index 8e1b527ed3..47ca2bb359 100644 --- a/build-data/paper.at +++ b/build-data/paper.at @@ -110,6 +110,8 @@ public net.minecraft.server.network.ServerLoginPacketListenerImpl connection public net.minecraft.server.network.ServerLoginPacketListenerImpl state public net.minecraft.server.network.ServerLoginPacketListenerImpl$State public net.minecraft.server.packs.VanillaPackResourcesBuilder safeGetPath(Ljava/net/URI;)Ljava/nio/file/Path; +public net.minecraft.server.packs.repository.FolderRepositorySource$FolderPackDetector +public net.minecraft.server.packs.repository.FolderRepositorySource$FolderPackDetector (Lnet/minecraft/world/level/validation/DirectoryValidator;)V public net.minecraft.server.packs.repository.Pack resources public net.minecraft.server.players.PlayerList playerIo public net.minecraft.server.players.PlayerList players diff --git a/paper-api/src/main/java/io/papermc/paper/datapack/Datapack.java b/paper-api/src/main/java/io/papermc/paper/datapack/Datapack.java index 95039ec90f..7c99257866 100644 --- a/paper-api/src/main/java/io/papermc/paper/datapack/Datapack.java +++ b/paper-api/src/main/java/io/papermc/paper/datapack/Datapack.java @@ -1,10 +1,8 @@ package io.papermc.paper.datapack; -import java.util.Set; import net.kyori.adventure.text.Component; -import org.bukkit.FeatureFlag; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.Unmodifiable; import org.jspecify.annotations.NullMarked; /** @@ -12,50 +10,9 @@ import org.jspecify.annotations.NullMarked; * won't be updated as datapacks are updated. */ @NullMarked -public interface Datapack { +@ApiStatus.NonExtendable +public interface Datapack extends DiscoveredDatapack { - /** - * Gets the name/id of this datapack. - * - * @return the name of the pack - */ - @Contract(pure = true) - String getName(); - - /** - * Gets the title component of this datapack. - * - * @return the title - */ - Component getTitle(); - - /** - * Gets the description component of this datapack. - * - * @return the description - */ - Component getDescription(); - - /** - * Gets if this datapack is required to be enabled. - * - * @return true if the pack is required - */ - boolean isRequired(); - - /** - * Gets the compatibility status of this pack. - * - * @return the compatibility of the pack - */ - Compatibility getCompatibility(); - - /** - * Gets the set of required features for this datapack. - * - * @return the set of required features - */ - @Unmodifiable Set getRequiredFeatures(); /** * Gets the enabled state of this pack. @@ -74,13 +31,6 @@ public interface Datapack { */ void setEnabled(boolean enabled); - /** - * Gets the source for this datapack. - * - * @return the pack source - */ - DatapackSource getSource(); - /** * Computes the component vanilla Minecraft uses * to display this datapack. Includes the {@link #getSource()}, @@ -96,4 +46,11 @@ public interface Datapack { TOO_NEW, COMPATIBLE, } + + /** + * Position of the pack in the load order. + */ + enum Position { + TOP, BOTTOM + } } diff --git a/paper-api/src/main/java/io/papermc/paper/datapack/DatapackRegistrar.java b/paper-api/src/main/java/io/papermc/paper/datapack/DatapackRegistrar.java new file mode 100644 index 0000000000..456c26c929 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/datapack/DatapackRegistrar.java @@ -0,0 +1,204 @@ +package io.papermc.paper.datapack; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.lifecycle.event.registrar.Registrar; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Consumer; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Unmodifiable; + +/** + * The registrar for datapacks. The event for this registrar + * is called anytime the game tries to discover datapacks at any of the + * configured locations. This means that if a datapack should stay available to the server, + * it must always be discovered whenever this event fires. + *

An example of a plugin loading a datapack from within it's own jar is below

+ *
{@code
+ * public class YourPluginBootstrap implements PluginBootstrap {
+ *     @Override
+ *     public void bootstrap(BoostrapContext context) {
+ *         final LifecycleEventManager manager = context.getLifecycleManager();
+ *         manager.registerEventHandler(LifecycleEvents.DATAPACK_DISCOVERY, event -> {
+ *             DatapackRegistrar registrar = event.registrar();
+ *             try {
+ *                 final URI uri = Objects.requireNonNull(
+ *                     YourPluginBootstrap.class.getResource("/pack")
+ *                 ).toURI();
+ *                 registrar.discoverPack(uri, "packId");
+ *             } catch (final URISyntaxException | IOException e) {
+ *                 throw new RuntimeException(e);
+ *             }
+ *         });
+ *     }
+ * }
+ * }
+ * @see io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents#DATAPACK_DISCOVERY + */ +@ApiStatus.NonExtendable +@ApiStatus.Experimental +public interface DatapackRegistrar extends Registrar { + + /** + * Checks if a datapack with the specified name has been discovered. + * + * @param name the name of the pack + * @return true if the pack has been discovered + * @see Datapack#getName() + */ + @Contract(pure = true) + boolean hasPackDiscovered(@NonNull String name); + + /** + * Gets a discovered datapack by its name. + * + * @param name the name of the pack + * @return the datapack + * @throws java.util.NoSuchElementException if the pack is not discovered + * @see Datapack#getName() + */ + @Contract(pure = true) + @NonNull DiscoveredDatapack getDiscoveredPack(@NonNull String name); + + /** + * Removes a discovered datapack by its name. + * + * @param name the name of the pack + * @return true if the pack was removed + * @see Datapack#getName() + */ + @Contract(mutates = "this") + boolean removeDiscoveredPack(@NonNull String name); + + /** + * Gets all discovered datapacks. + * + * @return an unmodifiable map of discovered packs + */ + @Contract(pure = true) + @Unmodifiable @NonNull Map getDiscoveredPacks(); + + /** + * Discovers a datapack at the specified {@link URI} with the id. + *

Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.

+ * + * @param uri the location of the pack + * @param id a unique id (will be combined with plugin for the datapacks name) + * @return the discovered datapack (or null if it failed) + * @throws IOException if any IO error occurs + */ + default @Nullable DiscoveredDatapack discoverPack(final @NonNull URI uri, final @NonNull String id) throws IOException { + return this.discoverPack(uri, id, c -> {}); + } + + /** + * Discovers a datapack at the specified {@link URI} with the id. + *

Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.

+ * + * @param uri the location of the pack + * @param id a unique id (will be combined with plugin for the datapacks name) + * @param configurer a configurer for extra options + * @return the discovered datapack (or null if it failed) + * @throws IOException if any IO error occurs + */ + @Nullable DiscoveredDatapack discoverPack(@NonNull URI uri, @NonNull String id, @NonNull Consumer configurer) throws IOException; + + /** + * Discovers a datapack at the specified {@link Path} with the id. + *

Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.

+ * + * @param path the location of the pack + * @param id a unique id (will be combined with plugin for the datapacks name) + * @return the discovered datapack (or null if it failed) + * @throws IOException if any IO error occurs + */ + default @Nullable DiscoveredDatapack discoverPack(final @NonNull Path path, final @NonNull String id) throws IOException { + return this.discoverPack(path, id, c -> {}); + } + + /** + * Discovers a datapack at the specified {@link Path} with the id. + *

Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.

+ * + * @param path the location of the pack + * @param id a unique id (will be combined with plugin for the datapacks name) + * @param configurer a configurer for extra options + * @return the discovered datapack (or null if it failed) + * @throws IOException if any IO error occurs + */ + @Nullable DiscoveredDatapack discoverPack(@NonNull Path path, @NonNull String id, @NonNull Consumer configurer) throws IOException; + + /** + * Discovers a datapack at the specified {@link URI} with the id. + *

Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.

+ * + * @param pluginMeta the plugin which will be the "owner" of this datapack + * @param uri the location of the pack + * @param id a unique id (will be combined with plugin for the datapacks name) + * @param configurer a configurer for extra options + * @return the discovered datapack (or null if it failed) + * @throws IOException if any IO error occurs + */ + @Nullable DiscoveredDatapack discoverPack(@NonNull PluginMeta pluginMeta, @NonNull URI uri, @NonNull String id, @NonNull Consumer configurer) throws IOException; + + /** + * Discovers a datapack at the specified {@link Path} with the id. + *

Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.

+ * + * @param pluginMeta the plugin which will be the "owner" of this datapack + * @param path the location of the pack + * @param id a unique id (will be combined with plugin for the datapacks name) + * @param configurer a configurer for extra options + * @return the discovered datapack (or null if it failed) + * @throws IOException if any IO error occurs + */ + @Nullable DiscoveredDatapack discoverPack(@NonNull PluginMeta pluginMeta, @NonNull Path path, @NonNull String id, @NonNull Consumer configurer) throws IOException; + + /** + * Configures additional, optional, details about a datapack. + */ + @ApiStatus.NonExtendable + @ApiStatus.Experimental + interface Configurer { + + /** + * Changes the title of the datapack from the default which + * is just the "id" in the {@code registerPack} methods. + * + * @param title the new title + * @return the configurer for chaining + */ + @Contract(value = "_ -> this", mutates = "this") + @NonNull Configurer title(@NonNull Component title); + + /** + * Sets if this pack is required. Defaults to false. + * A required pack cannot be disabled once enabled. Marking + * a pack as required does not mean it will immediately be enabled + * upon discovery. It may be enabled if this event was fired + * due to a pending (re)load. + * + * @param required true to require the pack + * @return the configurer for chaining + */ + @Contract(value = "_ -> this", mutates = "this") + @NonNull Configurer required(boolean required); + + /** + * Configures the position in the + * load order of this datapack. + * + * @param fixed won't move around in the load order as packs are added/removed + * @param position try to insert at the top of the order or bottom + * @return the configurer for chaining + */ + @Contract(value = "_, _ -> this", mutates = "this") + @NonNull Configurer position(boolean fixed, Datapack.@NonNull Position position); + } +} diff --git a/paper-api/src/main/java/io/papermc/paper/datapack/DiscoveredDatapack.java b/paper-api/src/main/java/io/papermc/paper/datapack/DiscoveredDatapack.java new file mode 100644 index 0000000000..109b930095 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/datapack/DiscoveredDatapack.java @@ -0,0 +1,69 @@ +package io.papermc.paper.datapack; + +import java.util.Set; +import net.kyori.adventure.text.Component; +import org.bukkit.FeatureFlag; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.NullMarked; + +/** + * This is a snapshot of a discovered datapack on the server. It + * won't be updated as datapacks are updated. + */ +@NullMarked +@ApiStatus.NonExtendable +public interface DiscoveredDatapack { + + /** + * Gets the name/id of this datapack. + * + * @return the name of the pack + */ + @Contract(pure = true) + String getName(); + + /** + * Gets the title component of this datapack. + * + * @return the title + */ + Component getTitle(); + + /** + * Gets the description component of this datapack. + * + * @return the description + */ + Component getDescription(); + + /** + * Gets if this datapack is required to be enabled. + * + * @return true if the pack is required + */ + boolean isRequired(); + + /** + * Gets the compatibility status of this pack. + * + * @return the compatibility of the pack + */ + Datapack.Compatibility getCompatibility(); + + /** + * Gets the set of required features for this datapack. + * + * @return the set of required features + */ + @Unmodifiable + Set getRequiredFeatures(); + + /** + * Gets the source for this datapack. + * + * @return the pack source + */ + DatapackSource getSource(); +} diff --git a/paper-api/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEvents.java b/paper-api/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEvents.java index b65c5fd544..8ff52773b0 100644 --- a/paper-api/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEvents.java +++ b/paper-api/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEvents.java @@ -1,10 +1,12 @@ package io.papermc.paper.plugin.lifecycle.event.types; import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.datapack.DatapackRegistrar; import io.papermc.paper.plugin.bootstrap.BootstrapContext; import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; +import io.papermc.paper.plugin.lifecycle.event.registrar.RegistrarEvent; import io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.ApiStatus; @@ -32,6 +34,14 @@ public final class LifecycleEvents { */ public static final TagEventTypeProvider TAGS = LifecycleEventTypeProvider.provider().tagProvider(); + + /** + * This event is for informing the server about any available datapacks from other sources such as inside a plugin's jar. You + * can register a handler for this event only in {@link io.papermc.paper.plugin.bootstrap.PluginBootstrap#bootstrap(BootstrapContext)}. + * @see DatapackRegistrar an example of a datapack being discovered + */ + public static final LifecycleEventType.Prioritizable> DATAPACK_DISCOVERY = bootstrapPrioritized("datapack_discovery"); + // @ApiStatus.Internal static LifecycleEventType.Monitorable plugin(final String name) { diff --git a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch index d8518cce12..019f5d45a1 100644 --- a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch @@ -1232,6 +1232,15 @@ }, this ); +@@ -1550,7 +_,7 @@ + DataPackConfig dataPackConfig = initialDataConfig.dataPacks(); + FeatureFlagSet featureFlagSet = initMode ? FeatureFlagSet.of() : initialDataConfig.enabledFeatures(); + FeatureFlagSet featureFlagSet1 = initMode ? FeatureFlags.REGISTRY.allFlags() : initialDataConfig.enabledFeatures(); +- packRepository.reload(); ++ packRepository.reload(true); // Paper - will load resource packs + if (safeMode) { + return configureRepositoryWithSelection(packRepository, List.of("vanilla"), featureFlagSet, false); + } else { @@ -1652,10 +_,11 @@ if (this.isEnforceWhitelist()) { PlayerList playerList = commandSource.getServer().getPlayerList(); diff --git a/paper-server/patches/sources/net/minecraft/server/commands/ReloadCommand.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/ReloadCommand.java.patch index 47fc313fa8..cfcc1b2b54 100644 --- a/paper-server/patches/sources/net/minecraft/server/commands/ReloadCommand.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/commands/ReloadCommand.java.patch @@ -9,6 +9,15 @@ LOGGER.warn("Failed to execute reload", throwable); source.sendFailure(Component.translatable("commands.reload.failure")); return null; +@@ -24,7 +_,7 @@ + } + + private static Collection discoverNewPacks(PackRepository packRepository, WorldData worldData, Collection selectedIds) { +- packRepository.reload(); ++ packRepository.reload(true); // Paper - will perform a full reload + Collection list = Lists.newArrayList(selectedIds); + Collection disabled = worldData.getDataConfiguration().dataPacks().getDisabled(); + @@ -36,6 +_,16 @@ return list; diff --git a/paper-server/patches/sources/net/minecraft/server/packs/repository/PackRepository.java.patch b/paper-server/patches/sources/net/minecraft/server/packs/repository/PackRepository.java.patch new file mode 100644 index 0000000000..d1064bb1e6 --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/server/packs/repository/PackRepository.java.patch @@ -0,0 +1,75 @@ +--- a/net/minecraft/server/packs/repository/PackRepository.java ++++ b/net/minecraft/server/packs/repository/PackRepository.java +@@ -21,9 +_,13 @@ + private final Set sources; + private Map available = ImmutableMap.of(); + private List selected = ImmutableList.of(); ++ private final net.minecraft.world.level.validation.DirectoryValidator validator; // Paper - add validator + +- public PackRepository(RepositorySource... sources) { +- this.sources = ImmutableSet.copyOf(sources); ++ // Paper start - add validator ++ public PackRepository(net.minecraft.world.level.validation.DirectoryValidator validator, RepositorySource... providers) { ++ this.validator = validator; ++ // Paper end - add validator ++ this.sources = ImmutableSet.copyOf(providers); + } + + public static String displayPackList(Collection packs) { +@@ -31,9 +_,14 @@ + } + + public void reload() { ++ // Paper start - perform a full reload ++ this.reload(false); ++ } ++ public void reload(boolean addRequiredPacks) { ++ // Paper end + List list = this.selected.stream().map(Pack::getId).collect(ImmutableList.toImmutableList()); + this.available = this.discoverAvailable(); +- this.selected = this.rebuildSelected(list); ++ this.selected = this.rebuildSelected(list, addRequiredPacks); // Paper + } + + private Map discoverAvailable() { +@@ -43,16 +_,23 @@ + repositorySource.loadPacks(pack -> map.put(pack.getId(), pack)); + } + +- return ImmutableMap.copyOf(map); ++ // Paper start - custom plugin-loaded datapacks ++ final io.papermc.paper.datapack.PaperDatapackRegistrar registrar = new io.papermc.paper.datapack.PaperDatapackRegistrar(this.validator, map); ++ io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.callStaticRegistrarEvent(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.DATAPACK_DISCOVERY, ++ registrar, ++ io.papermc.paper.plugin.bootstrap.BootstrapContext.class ++ ); ++ return ImmutableMap.copyOf(registrar.discoveredPacks); ++ // Paper end - custom plugin-loaded datapacks + } + + public boolean isAbleToClearAnyPack() { +- List list = this.rebuildSelected(List.of()); ++ List list = this.rebuildSelected(List.of(), false); // Paper - add willReload boolean + return !this.selected.equals(list); + } + + public void setSelected(Collection ids) { +- this.selected = this.rebuildSelected(ids); ++ this.selected = this.rebuildSelected(ids, false); // Paper - add willReload boolean + } + + public boolean addPack(String id) { +@@ -79,11 +_,11 @@ + } + } + +- private List rebuildSelected(Collection ids) { ++ private List rebuildSelected(Collection ids, boolean addRequiredPacks) { // Paper - add addRequiredPacks boolean + List list = this.getAvailablePacks(ids).collect(Util.toMutableList()); + + for (Pack pack : this.available.values()) { +- if (pack.isRequired() && !list.contains(pack)) { ++ if (pack.isRequired() && !list.contains(pack) && addRequiredPacks) { // Paper - add addRequiredPacks boolean + pack.getDefaultPosition().insert(list, pack, Pack::selectionConfig, false); + } + } diff --git a/paper-server/patches/sources/net/minecraft/server/packs/repository/ServerPacksSource.java.patch b/paper-server/patches/sources/net/minecraft/server/packs/repository/ServerPacksSource.java.patch index dc3a6578a8..ee3142e9b5 100644 --- a/paper-server/patches/sources/net/minecraft/server/packs/repository/ServerPacksSource.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/packs/repository/ServerPacksSource.java.patch @@ -9,7 +9,7 @@ .applyDevelopmentConfig() .pushJarResources() .build(VANILLA_PACK_INFO); -@@ -68,7 +_,18 @@ +@@ -68,15 +_,26 @@ @Nullable @Override protected Pack createBuiltinPack(String id, Pack.ResourcesSupplier resources, Component title) { @@ -29,3 +29,13 @@ } public static PackRepository createPackRepository(Path folder, DirectoryValidator validator) { +- return new PackRepository(new ServerPacksSource(validator), new FolderRepositorySource(folder, PackType.SERVER_DATA, PackSource.WORLD, validator)); ++ return new PackRepository(validator, new ServerPacksSource(validator), new FolderRepositorySource(folder, PackType.SERVER_DATA, PackSource.WORLD, validator)); // Paper - add validator + } + + public static PackRepository createVanillaTrustedRepository() { +- return new PackRepository(new ServerPacksSource(new DirectoryValidator(path -> true))); ++ return new PackRepository(new DirectoryValidator(path -> true), new ServerPacksSource(new DirectoryValidator(path -> true))); // Paper - add validator + } + + public static PackRepository createPackRepository(LevelStorageSource.LevelStorageAccess level) { diff --git a/paper-server/src/main/java/io/papermc/paper/datapack/PaperDatapack.java b/paper-server/src/main/java/io/papermc/paper/datapack/PaperDatapack.java index 8bd8263b51..bbd709529c 100644 --- a/paper-server/src/main/java/io/papermc/paper/datapack/PaperDatapack.java +++ b/paper-server/src/main/java/io/papermc/paper/datapack/PaperDatapack.java @@ -2,71 +2,25 @@ package io.papermc.paper.datapack; import io.papermc.paper.adventure.PaperAdventure; import io.papermc.paper.event.server.ServerResourcesReloadedEvent; -import io.papermc.paper.world.flag.PaperFeatureFlagProviderImpl; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import net.kyori.adventure.text.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.packs.repository.Pack; -import net.minecraft.server.packs.repository.PackSource; -import org.bukkit.FeatureFlag; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.framework.qual.DefaultQualifier; +import org.jspecify.annotations.NullMarked; -@DefaultQualifier(NonNull.class) -public class PaperDatapack implements Datapack { - - private static final Map PACK_SOURCES = new ConcurrentHashMap<>(); - static { - PACK_SOURCES.put(PackSource.DEFAULT, DatapackSource.DEFAULT); - PACK_SOURCES.put(PackSource.BUILT_IN, DatapackSource.BUILT_IN); - PACK_SOURCES.put(PackSource.FEATURE, DatapackSource.FEATURE); - PACK_SOURCES.put(PackSource.WORLD, DatapackSource.WORLD); - PACK_SOURCES.put(PackSource.SERVER, DatapackSource.SERVER); - } +@NullMarked +public class PaperDatapack extends PaperDiscoveredDatapack implements Datapack { private final Pack pack; private final boolean enabled; PaperDatapack(final Pack pack, final boolean enabled) { + super(pack); this.pack = pack; this.enabled = enabled; } - @Override - public String getName() { - return this.pack.getId(); - } - - @Override - public Component getTitle() { - return PaperAdventure.asAdventure(this.pack.getTitle()); - } - - @Override - public Component getDescription() { - return PaperAdventure.asAdventure(this.pack.getDescription()); - } - - @Override - public boolean isRequired() { - return this.pack.isRequired(); - } - - @Override - public Compatibility getCompatibility() { - return Datapack.Compatibility.valueOf(this.pack.getCompatibility().name()); - } - - @Override - public Set getRequiredFeatures() { - return PaperFeatureFlagProviderImpl.fromNms(this.pack.getRequestedFeatures()); - } - @Override public boolean isEnabled() { return this.enabled; @@ -76,7 +30,7 @@ public class PaperDatapack implements Datapack { public void setEnabled(final boolean enabled) { final MinecraftServer server = MinecraftServer.getServer(); final List enabledPacks = new ArrayList<>(server.getPackRepository().getSelectedPacks()); - final @Nullable Pack packToChange = server.getPackRepository().getPack(this.getName()); + final Pack packToChange = server.getPackRepository().getPack(this.getName()); if (packToChange == null) { throw new IllegalStateException("Cannot toggle state of pack that doesn't exist: " + this.getName()); } @@ -91,11 +45,6 @@ public class PaperDatapack implements Datapack { server.reloadResources(enabledPacks.stream().map(Pack::getId).toList(), ServerResourcesReloadedEvent.Cause.PLUGIN); } - @Override - public DatapackSource getSource() { - return PACK_SOURCES.computeIfAbsent(this.pack.location().source(), source -> new DatapackSourceImpl(source.toString())); - } - @Override public Component computeDisplayName() { return PaperAdventure.asAdventure(this.pack.getChatLink(this.enabled)); diff --git a/paper-server/src/main/java/io/papermc/paper/datapack/PaperDatapackRegistrar.java b/paper-server/src/main/java/io/papermc/paper/datapack/PaperDatapackRegistrar.java new file mode 100644 index 0000000000..c8a854bd90 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datapack/PaperDatapackRegistrar.java @@ -0,0 +1,165 @@ +package io.papermc.paper.datapack; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.mojang.logging.LogUtils; +import io.papermc.paper.adventure.PaperAdventure; +import io.papermc.paper.plugin.bootstrap.BootstrapContext; +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.lifecycle.event.registrar.PaperRegistrar; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Consumer; +import net.kyori.adventure.text.Component; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackSelectionConfig; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.VanillaPackResourcesBuilder; +import net.minecraft.server.packs.repository.FolderRepositorySource; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackDetector; +import net.minecraft.world.level.validation.ContentValidationException; +import net.minecraft.world.level.validation.DirectoryValidator; +import net.minecraft.world.level.validation.ForbiddenSymlinkInfo; +import org.jetbrains.annotations.Unmodifiable; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; + +@NullMarked +public class PaperDatapackRegistrar implements PaperRegistrar, DatapackRegistrar { + + private static final Logger LOGGER = LogUtils.getClassLogger(); + + private final PackDetector detector; + public final Map discoveredPacks; + private @Nullable BootstrapContext owner; + + public PaperDatapackRegistrar(final DirectoryValidator symlinkValidator, final Map discoveredPacks) { + this.detector = new FolderRepositorySource.FolderPackDetector(symlinkValidator); + this.discoveredPacks = discoveredPacks; + } + + @Override + public void setCurrentContext(final @Nullable BootstrapContext owner) { + this.owner = owner; + } + + @Override + public boolean hasPackDiscovered(final String name) { + return this.discoveredPacks.containsKey(name); + } + + @Override + public DiscoveredDatapack getDiscoveredPack(final String name) { + if (!this.hasPackDiscovered(name)) { + throw new NoSuchElementException("No pack with id " + name + " was discovered"); + } + return new PaperDiscoveredDatapack(this.discoveredPacks.get(name)); + } + + @Override + public boolean removeDiscoveredPack(final String name) { + return this.discoveredPacks.remove(name) != null; + } + + @Override + public @Unmodifiable Map getDiscoveredPacks() { + final ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(this.discoveredPacks.size()); + for (final Map.Entry entry : this.discoveredPacks.entrySet()) { + builder.put(entry.getKey(), new PaperDiscoveredDatapack(entry.getValue())); + } + return builder.build(); + } + + @Override + public @Nullable DiscoveredDatapack discoverPack(final URI uri, final String id, final Consumer configurer) throws IOException { + Preconditions.checkState(this.owner != null, "Cannot register a datapack without specifying a PluginMeta yet"); + return this.discoverPack(this.owner.getPluginMeta(), uri, id, configurer); + } + + @Override + public @Nullable DiscoveredDatapack discoverPack(final Path path, final String id, final Consumer configurer) throws IOException { + Preconditions.checkState(this.owner != null, "Cannot register a datapack without specifying a PluginMeta yet"); + return this.discoverPack(this.owner.getPluginMeta(), path, id, configurer); + } + + @Override + public @Nullable DiscoveredDatapack discoverPack(final PluginMeta pluginMeta, final URI uri, final String id, final Consumer configurer) throws IOException { + return this.discoverPack(pluginMeta, VanillaPackResourcesBuilder.safeGetPath(uri), id, configurer); + } + + @Override + public @Nullable DiscoveredDatapack discoverPack(final PluginMeta pluginMeta, final Path path, final String id, final Consumer configurer) throws IOException { + final List badLinks = new ArrayList<>(); + final Pack.ResourcesSupplier resourcesSupplier = this.detector.detectPackResources(path, badLinks); + if (!badLinks.isEmpty()) { + LOGGER.warn("Ignoring potential pack entry: {}", ContentValidationException.getMessage(path, badLinks)); + } else if (resourcesSupplier != null) { + final String packId = pluginMeta.getName() + "/" + id; + final ConfigurerImpl configurerImpl = new ConfigurerImpl(Component.text(packId)); + configurer.accept(configurerImpl); + final PackLocationInfo locInfo = new PackLocationInfo(packId, + PaperAdventure.asVanilla(configurerImpl.title), + PluginPackSource.INSTANCE, + Optional.empty() + ); + final Pack pack = Pack.readMetaAndCreate(locInfo, + resourcesSupplier, + PackType.SERVER_DATA, + new PackSelectionConfig( + configurerImpl.required, + configurerImpl.position, + configurerImpl.fixedPosition + )); + if (pack != null) { + this.discoveredPacks.put(packId, pack); + return new PaperDiscoveredDatapack(pack); + } + return null; + } else { + LOGGER.info("Found non-pack entry '{}', ignoring", path); + } + return null; + } + + static final class ConfigurerImpl implements Configurer { + + private Component title; + private boolean required = false; + private boolean fixedPosition = false; + private Pack.Position position = Pack.Position.TOP; + + ConfigurerImpl(final Component title) { + this.title = title; + } + + @Override + public Configurer title(final Component title) { + this.title = title; + return this; + } + + @Override + public Configurer required(final boolean required) { + this.required = required; + return this; + } + + @Override + public Configurer position(final boolean fixed, final Datapack.Position position) { + this.fixedPosition = fixed; + this.position = switch (position) { + case TOP -> Pack.Position.TOP; + case BOTTOM -> Pack.Position.BOTTOM; + }; + return this; + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datapack/PaperDiscoveredDatapack.java b/paper-server/src/main/java/io/papermc/paper/datapack/PaperDiscoveredDatapack.java new file mode 100644 index 0000000000..7267a016bd --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datapack/PaperDiscoveredDatapack.java @@ -0,0 +1,68 @@ +package io.papermc.paper.datapack; + +import com.google.common.collect.ImmutableMap; +import io.papermc.paper.adventure.PaperAdventure; +import io.papermc.paper.world.flag.PaperFeatureFlagProviderImpl; +import java.util.Map; +import java.util.Set; +import net.kyori.adventure.text.Component; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackSource; +import org.bukkit.FeatureFlag; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class PaperDiscoveredDatapack implements DiscoveredDatapack { + + private static final Map PACK_SOURCES; + static { + PACK_SOURCES = ImmutableMap.builder() + .put(PackSource.DEFAULT, DatapackSource.DEFAULT) + .put(PackSource.BUILT_IN, DatapackSource.BUILT_IN) + .put(PackSource.FEATURE, DatapackSource.FEATURE) + .put(PackSource.WORLD, DatapackSource.WORLD) + .put(PackSource.SERVER, DatapackSource.SERVER) + .buildOrThrow(); + } + + private final Pack pack; + + PaperDiscoveredDatapack(final Pack pack) { + this.pack = pack; + } + + @Override + public String getName() { + return this.pack.getId(); + } + + @Override + public Component getTitle() { + return PaperAdventure.asAdventure(this.pack.getTitle()); + } + + @Override + public Component getDescription() { + return PaperAdventure.asAdventure(this.pack.getDescription()); + } + + @Override + public boolean isRequired() { + return this.pack.isRequired(); + } + + @Override + public Datapack.Compatibility getCompatibility() { + return Datapack.Compatibility.valueOf(this.pack.getCompatibility().name()); + } + + @Override + public Set getRequiredFeatures() { + return PaperFeatureFlagProviderImpl.fromNms(this.pack.getRequestedFeatures()); + } + + @Override + public DatapackSource getSource() { + return PACK_SOURCES.computeIfAbsent(this.pack.location().source(), source -> new DatapackSourceImpl(source.toString())); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/datapack/PluginPackSource.java b/paper-server/src/main/java/io/papermc/paper/datapack/PluginPackSource.java new file mode 100644 index 0000000000..79a92a2a40 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/datapack/PluginPackSource.java @@ -0,0 +1,25 @@ +package io.papermc.paper.datapack; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.server.packs.repository.PackSource; +import org.jspecify.annotations.NullMarked; + +@NullMarked +final class PluginPackSource implements PackSource { + + static final PackSource INSTANCE = new PluginPackSource(); + + private PluginPackSource() { + } + + @Override + public Component decorate(final Component packDisplayName) { + return Component.translatable("pack.nameAndSource", packDisplayName, "plugin").withStyle(ChatFormatting.GRAY); + } + + @Override + public boolean shouldAddAutomatically() { + return true; + } +} diff --git a/test-plugin/src/main/java/io/papermc/testplugin/TestPluginBootstrap.java b/test-plugin/src/main/java/io/papermc/testplugin/TestPluginBootstrap.java index fe2b287b25..bfb7009bcb 100644 --- a/test-plugin/src/main/java/io/papermc/testplugin/TestPluginBootstrap.java +++ b/test-plugin/src/main/java/io/papermc/testplugin/TestPluginBootstrap.java @@ -1,7 +1,14 @@ package io.papermc.testplugin; +import io.papermc.paper.datapack.DatapackRegistrar; import io.papermc.paper.plugin.bootstrap.BootstrapContext; import io.papermc.paper.plugin.bootstrap.PluginBootstrap; +import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Objects; import org.jetbrains.annotations.NotNull; public class TestPluginBootstrap implements PluginBootstrap { @@ -9,6 +16,16 @@ public class TestPluginBootstrap implements PluginBootstrap { @Override public void bootstrap(@NotNull BootstrapContext context) { // io.papermc.testplugin.brigtests.Registration.registerViaBootstrap(context); + final LifecycleEventManager manager = context.getLifecycleManager(); + manager.registerEventHandler(LifecycleEvents.DATAPACK_DISCOVERY, event -> { + final DatapackRegistrar registrar = event.registrar(); + try { + final URI uri = Objects.requireNonNull(TestPluginBootstrap.class.getResource("/pack")).toURI(); + registrar.discoverPack(uri, "test"); + } catch (final URISyntaxException | IOException e) { + throw new RuntimeException(e); + } + }); } } diff --git a/test-plugin/src/main/resources/pack/data/machine_maker/tags/entity_type/test.json b/test-plugin/src/main/resources/pack/data/machine_maker/tags/entity_type/test.json new file mode 100644 index 0000000000..36722a9a39 --- /dev/null +++ b/test-plugin/src/main/resources/pack/data/machine_maker/tags/entity_type/test.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:zombie", + "minecraft:spider" + ] +} diff --git a/test-plugin/src/main/resources/pack/data/machine_maker/tags/item/test.json b/test-plugin/src/main/resources/pack/data/machine_maker/tags/item/test.json new file mode 100644 index 0000000000..2a659578fa --- /dev/null +++ b/test-plugin/src/main/resources/pack/data/machine_maker/tags/item/test.json @@ -0,0 +1,6 @@ +{ + "values": [ + "minecraft:stone_bricks", + "minecraft:string" + ] +} diff --git a/test-plugin/src/main/resources/pack/tags/pack.mcmeta b/test-plugin/src/main/resources/pack/tags/pack.mcmeta new file mode 100644 index 0000000000..8a117a64fc --- /dev/null +++ b/test-plugin/src/main/resources/pack/tags/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack":{ + "pack_format": 41, + "description": "Test datapack for tags" + } +}