Add datapack registration lifecycle event (#11804)
This commit is contained in:
@@ -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<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);
|
||||
}
|
||||
@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<FeatureFlag> 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<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) {
|
||||
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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user