Add datapack registration lifecycle event (#11804)

This commit is contained in:
Jake Potrebic 2024-12-28 13:30:43 -08:00 committed by GitHub
parent dac977a106
commit feb8756567
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 691 additions and 114 deletions

View File

@ -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.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.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 <init>(Lnet/minecraft/world/level/validation/DirectoryValidator;)V
public net.minecraft.server.packs.repository.Pack resources public net.minecraft.server.packs.repository.Pack resources
public net.minecraft.server.players.PlayerList playerIo public net.minecraft.server.players.PlayerList playerIo
public net.minecraft.server.players.PlayerList players public net.minecraft.server.players.PlayerList players

View File

@ -1,10 +1,8 @@
package io.papermc.paper.datapack; package io.papermc.paper.datapack;
import java.util.Set;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.bukkit.FeatureFlag; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Unmodifiable;
import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.NullMarked;
/** /**
@ -12,50 +10,8 @@ import org.jspecify.annotations.NullMarked;
* won't be updated as datapacks are updated. * won't be updated as datapacks are updated.
*/ */
@NullMarked @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<FeatureFlag> getRequiredFeatures();
/** /**
* Gets the enabled state of this pack. * Gets the enabled state of this pack.
@ -74,13 +30,6 @@ public interface Datapack {
*/ */
void setEnabled(boolean enabled); void setEnabled(boolean enabled);
/**
* Gets the source for this datapack.
*
* @return the pack source
*/
DatapackSource getSource();
/** /**
* Computes the component vanilla Minecraft uses * Computes the component vanilla Minecraft uses
* to display this datapack. Includes the {@link #getSource()}, * to display this datapack. Includes the {@link #getSource()},
@ -96,4 +45,11 @@ public interface Datapack {
TOO_NEW, TOO_NEW,
COMPATIBLE, COMPATIBLE,
} }
/**
* Position of the pack in the load order.
*/
enum Position {
TOP, BOTTOM
}
} }

View File

@ -0,0 +1,202 @@
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.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Unmodifiable;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
/**
* 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.
* <p>An example of a plugin loading a datapack from within it's own jar is below</p>
* <pre>{@code
* public class YourPluginBootstrap implements PluginBootstrap {
* @Override
* public void bootstrap(BoostrapContext context) {
* final LifecycleEventManager<BootstrapContext> 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);
* }
* });
* }
* }
* }</pre>
* @see io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents#DATAPACK_DISCOVERY
*/
@ApiStatus.NonExtendable
@ApiStatus.Experimental
@NullMarked
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(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)
DiscoveredDatapack getDiscoveredPack(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(String name);
/**
* Gets all discovered datapacks.
*
* @return an unmodifiable map of discovered packs
*/
@Contract(pure = true)
@Unmodifiable Map<String, DiscoveredDatapack> getDiscoveredPacks();
/**
* Discovers a datapack at the specified {@link URI} with the id.
* <p>Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.</p>
*
* @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 URI uri, final String id) throws IOException {
return this.discoverPack(uri, id, c -> {});
}
/**
* Discovers a datapack at the specified {@link URI} with the id.
* <p>Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.</p>
*
* @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(URI uri, String id, Consumer<Configurer> configurer) throws IOException;
/**
* Discovers a datapack at the specified {@link Path} with the id.
* <p>Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.</p>
*
* @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 Path path, final String id) throws IOException {
return this.discoverPack(path, id, c -> {});
}
/**
* Discovers a datapack at the specified {@link Path} with the id.
* <p>Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.</p>
*
* @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(Path path, String id, Consumer<Configurer> configurer) throws IOException;
/**
* Discovers a datapack at the specified {@link URI} with the id.
* <p>Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.</p>
*
* @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(PluginMeta pluginMeta, URI uri, String id, Consumer<Configurer> configurer) throws IOException;
/**
* Discovers a datapack at the specified {@link Path} with the id.
* <p>Symlinks obey the {@code allowed_symlinks.txt} in the server root directory.</p>
*
* @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(PluginMeta pluginMeta, Path path, String id, Consumer<Configurer> 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")
Configurer title(Component title);
/**
* Sets whether this pack is going to be automatically enabled on server starts even if previously disabled.
* Defaults to false.
*
* @param autoEnableOnServerStart true to ensure the pack is enabled on server starts.
* @return the configurer for chaining
*/
@Contract(value = "_ -> this", mutates = "this")
Configurer autoEnableOnServerStart(boolean autoEnableOnServerStart);
/**
* 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")
Configurer position(boolean fixed, Datapack.Position position);
}
}

View File

@ -13,6 +13,7 @@ public sealed interface DatapackSource permits DatapackSourceImpl {
DatapackSource FEATURE = create("feature"); DatapackSource FEATURE = create("feature");
DatapackSource WORLD = create("world"); DatapackSource WORLD = create("world");
DatapackSource SERVER = create("server"); DatapackSource SERVER = create("server");
DatapackSource PLUGIN = create("plugin");
private static DatapackSource create(final String name) { private static DatapackSource create(final String name) {
return new DatapackSourceImpl(name); return new DatapackSourceImpl(name);

View File

@ -0,0 +1,71 @@
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.
* <p>
* A "required" datapack will always be enabled on server startup, even if previously disabled.
*
* @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<FeatureFlag> getRequiredFeatures();
/**
* Gets the source for this datapack.
*
* @return the pack source
*/
DatapackSource getSource();
}

View File

@ -1,10 +1,12 @@
package io.papermc.paper.plugin.lifecycle.event.types; package io.papermc.paper.plugin.lifecycle.event.types;
import io.papermc.paper.command.brigadier.Commands; 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.bootstrap.BootstrapContext;
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager;
import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; 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 io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
@ -32,6 +34,14 @@ public final class LifecycleEvents {
*/ */
public static final TagEventTypeProvider TAGS = LifecycleEventTypeProvider.provider().tagProvider(); 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<BootstrapContext, RegistrarEvent<DatapackRegistrar>> DATAPACK_DISCOVERY = bootstrapPrioritized("datapack_discovery");
//<editor-fold desc="helper methods" defaultstate="collapsed"> //<editor-fold desc="helper methods" defaultstate="collapsed">
@ApiStatus.Internal @ApiStatus.Internal
static <E extends LifecycleEvent> LifecycleEventType.Monitorable<Plugin, E> plugin(final String name) { static <E extends LifecycleEvent> LifecycleEventType.Monitorable<Plugin, E> plugin(final String name) {

View File

@ -1193,15 +1193,18 @@
return ReloadableServerResources.loadResources( return ReloadableServerResources.loadResources(
closeableResourceManager, closeableResourceManager,
this.registries, this.registries,
@@ -1520,6 +_,7 @@ @@ -1520,20 +_,39 @@
) )
.thenAcceptAsync( .thenAcceptAsync(
reloadableResources -> { reloadableResources -> {
+ io.papermc.paper.command.brigadier.PaperBrigadier.moveBukkitCommands(this.resources.managers().getCommands(), reloadableResources.managers().commands); // Paper + io.papermc.paper.command.brigadier.PaperBrigadier.moveBukkitCommands(this.resources.managers().getCommands(), reloadableResources.managers().commands); // Paper
this.resources.close(); this.resources.close();
this.resources = reloadableResources; this.resources = reloadableResources;
this.packRepository.setSelected(selectedIds); - this.packRepository.setSelected(selectedIds);
@@ -1529,11 +_,29 @@ + this.packRepository.setSelected(selectedIds, false); // Paper - add pendingReload flag to determine required pack loading - false as this is *after* a reload (see above)
WorldDataConfiguration worldDataConfiguration = new WorldDataConfiguration(
getSelectedPacks(this.packRepository, true), this.worldData.enabledFeatures()
);
this.worldData.setDataConfiguration(worldDataConfiguration); this.worldData.setDataConfiguration(worldDataConfiguration);
this.resources.managers.updateStaticRegistryTags(); this.resources.managers.updateStaticRegistryTags();
this.resources.managers.getRecipeManager().finalizeRecipeLoading(this.worldData.enabledFeatures()); this.resources.managers.getRecipeManager().finalizeRecipeLoading(this.worldData.enabledFeatures());
@ -1232,6 +1235,33 @@
}, },
this 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 {
@@ -1605,7 +_,7 @@
private static WorldDataConfiguration configureRepositoryWithSelection(
PackRepository packRepository, Collection<String> selectedPacks, FeatureFlagSet enabledFeatures, boolean safeMode
) {
- packRepository.setSelected(selectedPacks);
+ packRepository.setSelected(selectedPacks, true); // Paper - add pendingReload flag to determine required pack loading - before the initial server load
enableForcedFeaturePacks(packRepository, enabledFeatures);
DataPackConfig selectedPacks1 = getSelectedPacks(packRepository, safeMode);
FeatureFlagSet featureFlagSet = packRepository.getRequestedFeatureFlags().join(enabledFeatures);
@@ -1637,7 +_,7 @@
}
}
- packRepository.setSelected(set);
+ packRepository.setSelected(set, true); // Paper - add pendingReload flag to determine required pack loading - before the initial server start
}
}
@@ -1652,10 +_,11 @@ @@ -1652,10 +_,11 @@
if (this.isEnforceWhitelist()) { if (this.isEnforceWhitelist()) {
PlayerList playerList = commandSource.getServer().getPlayerList(); PlayerList playerList = commandSource.getServer().getPlayerList();

View File

@ -9,6 +9,15 @@
LOGGER.warn("Failed to execute reload", throwable); LOGGER.warn("Failed to execute reload", throwable);
source.sendFailure(Component.translatable("commands.reload.failure")); source.sendFailure(Component.translatable("commands.reload.failure"));
return null; return null;
@@ -24,7 +_,7 @@
}
private static Collection<String> discoverNewPacks(PackRepository packRepository, WorldData worldData, Collection<String> selectedIds) {
- packRepository.reload();
+ packRepository.reload(true); // Paper - will perform a full reload
Collection<String> list = Lists.newArrayList(selectedIds);
Collection<String> disabled = worldData.getDataConfiguration().dataPacks().getDisabled();
@@ -36,6 +_,16 @@ @@ -36,6 +_,16 @@
return list; return list;

View File

@ -0,0 +1,76 @@
--- a/net/minecraft/server/packs/repository/PackRepository.java
+++ b/net/minecraft/server/packs/repository/PackRepository.java
@@ -21,9 +_,13 @@
private final Set<RepositorySource> sources;
private Map<String, Pack> available = ImmutableMap.of();
private List<Pack> 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<Pack> packs) {
@@ -31,9 +_,14 @@
}
public void reload() {
+ // Paper start - add pendingReload flag to determine required pack loading
+ this.reload(false);
+ }
+ public void reload(final boolean pendingReload) {
+ // Paper end - add pendingReload flag to determine required pack loading
List<String> list = this.selected.stream().map(Pack::getId).collect(ImmutableList.toImmutableList());
this.available = this.discoverAvailable();
- this.selected = this.rebuildSelected(list);
+ this.selected = this.rebuildSelected(list, pendingReload); // Paper - add pendingReload flag to determine required pack loading
}
private Map<String, Pack> 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<Pack> list = this.rebuildSelected(List.of());
+ List<Pack> list = this.rebuildSelected(List.of(), false); // Paper - add pendingReload flag to determine required pack loading
return !this.selected.equals(list);
}
- public void setSelected(Collection<String> ids) {
- this.selected = this.rebuildSelected(ids);
+ public void setSelected(Collection<String> ids, final boolean pendingReload) { // Paper - add pendingReload flag to determine required pack loading
+ this.selected = this.rebuildSelected(ids, pendingReload); // Paper - add pendingReload flag to determine required pack loading
}
public boolean addPack(String id) {
@@ -79,11 +_,11 @@
}
}
- private List<Pack> rebuildSelected(Collection<String> ids) {
+ private List<Pack> rebuildSelected(Collection<String> ids, boolean pendingReload) { // Paper - add pendingReload flag to determine required pack loading
List<Pack> 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) && pendingReload) { // Paper - add pendingReload flag to determine required pack loading
pack.getDefaultPosition().insert(list, pack, Pack::selectionConfig, false);
}
}

View File

@ -9,7 +9,7 @@
.applyDevelopmentConfig() .applyDevelopmentConfig()
.pushJarResources() .pushJarResources()
.build(VANILLA_PACK_INFO); .build(VANILLA_PACK_INFO);
@@ -68,7 +_,18 @@ @@ -68,15 +_,26 @@
@Nullable @Nullable
@Override @Override
protected Pack createBuiltinPack(String id, Pack.ResourcesSupplier resources, Component title) { protected Pack createBuiltinPack(String id, Pack.ResourcesSupplier resources, Component title) {
@ -29,3 +29,13 @@
} }
public static PackRepository createPackRepository(Path folder, DirectoryValidator validator) { 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) {

View File

@ -2,71 +2,25 @@ package io.papermc.paper.datapack;
import io.papermc.paper.adventure.PaperAdventure; import io.papermc.paper.adventure.PaperAdventure;
import io.papermc.paper.event.server.ServerResourcesReloadedEvent; import io.papermc.paper.event.server.ServerResourcesReloadedEvent;
import io.papermc.paper.world.flag.PaperFeatureFlagProviderImpl;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.kyori.adventure.text.Component;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.packs.repository.Pack; import net.minecraft.server.packs.repository.Pack;
import net.minecraft.server.packs.repository.PackSource; import org.jspecify.annotations.NullMarked;
import org.bukkit.FeatureFlag;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
@DefaultQualifier(NonNull.class) @NullMarked
public class PaperDatapack implements Datapack { public class PaperDatapack extends PaperDiscoveredDatapack implements Datapack {
private static final Map<PackSource, DatapackSource> 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);
}
private final Pack pack; private final Pack pack;
private final boolean enabled; private final boolean enabled;
PaperDatapack(final Pack pack, final boolean enabled) { PaperDatapack(final Pack pack, final boolean enabled) {
super(pack);
this.pack = pack; this.pack = pack;
this.enabled = enabled; 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<FeatureFlag> getRequiredFeatures() {
return PaperFeatureFlagProviderImpl.fromNms(this.pack.getRequestedFeatures());
}
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
return this.enabled; return this.enabled;
@ -76,7 +30,7 @@ public class PaperDatapack implements Datapack {
public void setEnabled(final boolean enabled) { public void setEnabled(final boolean enabled) {
final MinecraftServer server = MinecraftServer.getServer(); final MinecraftServer server = MinecraftServer.getServer();
final List<Pack> enabledPacks = new ArrayList<>(server.getPackRepository().getSelectedPacks()); final List<Pack> 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) { if (packToChange == null) {
throw new IllegalStateException("Cannot toggle state of pack that doesn't exist: " + this.getName()); 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); 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 @Override
public Component computeDisplayName() { public Component computeDisplayName() {
return PaperAdventure.asAdventure(this.pack.getChatLink(this.enabled)); return PaperAdventure.asAdventure(this.pack.getChatLink(this.enabled));

View File

@ -0,0 +1,167 @@
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<BootstrapContext>, DatapackRegistrar {
private static final Logger LOGGER = LogUtils.getClassLogger();
private final PackDetector<Pack.ResourcesSupplier> detector;
public final Map<String, Pack> discoveredPacks;
private @Nullable BootstrapContext owner;
public PaperDatapackRegistrar(final DirectoryValidator symlinkValidator, final Map<String, Pack> 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<String, DiscoveredDatapack> getDiscoveredPacks() {
final ImmutableMap.Builder<String, DiscoveredDatapack> builder = ImmutableMap.builderWithExpectedSize(this.discoveredPacks.size());
for (final Map.Entry<String, Pack> entry : this.discoveredPacks.entrySet()) {
builder.put(entry.getKey(), new PaperDiscoveredDatapack(entry.getValue()));
}
return builder.buildOrThrow();
}
@Override
public @Nullable DiscoveredDatapack discoverPack(final URI uri, final String id, final Consumer<Configurer> configurer) throws IOException {
Preconditions.checkState(this.owner != null, "Discovering packs is not supported outside of lifecycle events");
return this.discoverPack(this.owner.getPluginMeta(), uri, id, configurer);
}
@Override
public @Nullable DiscoveredDatapack discoverPack(final Path path, final String id, final Consumer<Configurer> configurer) throws IOException {
Preconditions.checkState(this.owner != null, "Discovering packs is not supported outside of lifecycle events");
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> 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> configurer) throws IOException {
Preconditions.checkState(this.owner != null, "Discovering packs is not supported outside of lifecycle events");
final List<ForbiddenSymlinkInfo> 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));
return null;
} 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.autoEnableOnServerStart,
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 autoEnableOnServerStart = 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 autoEnableOnServerStart(final boolean autoEnableOnServerStart) {
this.autoEnableOnServerStart = autoEnableOnServerStart;
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;
}
}
}

View File

@ -0,0 +1,69 @@
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<PackSource, DatapackSource> PACK_SOURCES;
static {
PACK_SOURCES = ImmutableMap.<PackSource, DatapackSource>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)
.put(PluginPackSource.INSTANCE, DatapackSource.PLUGIN)
.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<FeatureFlag> getRequiredFeatures() {
return PaperFeatureFlagProviderImpl.fromNms(this.pack.getRequestedFeatures());
}
@Override
public DatapackSource getSource() {
return PACK_SOURCES.computeIfAbsent(this.pack.location().source(), source -> new DatapackSourceImpl(source.toString()));
}
}

View File

@ -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;
}
}