Merge remote-tracking branch 'upstream/main' into update/1.21.4

This commit is contained in:
2025-03-30 11:43:18 +02:00
519 changed files with 7311 additions and 3127 deletions

View File

@@ -30,6 +30,6 @@ public class CraftGameEventTag extends CraftTag<net.minecraft.world.level.gameev
@Override
public @NotNull Set<GameEvent> getValues() {
return getHandle().stream().map((nms) -> Objects.requireNonNull(GameEvent.getByKey(CraftNamespacedKey.fromMinecraft(BuiltInRegistries.GAME_EVENT.getKey(nms.value()))), nms + " is not a recognized game event")).collect(Collectors.toUnmodifiableSet());
return getHandle().stream().map((nms) -> Objects.requireNonNull(GameEvent.getByKey(CraftNamespacedKey.fromMinecraft(BuiltInRegistries.GAME_EVENT.getKey(nms.value()))), () -> nms + " is not a recognized game event")).collect(Collectors.toUnmodifiableSet());
}
}

View File

@@ -76,6 +76,20 @@ public class DataComponentValueConverterProviderImpl implements DataComponentVal
throw new IllegalArgumentException(e);
}
}
),
DataComponentValueConverterRegistry.Conversion.convert(
DataComponentValue.TagSerializable.class,
GsonDataComponentValue.class,
(key, tagSerializable) -> {
Tag decodedSnbt;
try {
decodedSnbt = tagSerializable.asBinaryTag().get(PaperAdventure.NBT_CODEC);
} catch (final CommandSyntaxException e) {
throw new IllegalArgumentException("Unable to parse SNBT value", e);
}
return GsonDataComponentValue.gsonDataComponentValue(NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, decodedSnbt));
}
)
);
}

View File

@@ -11,6 +11,11 @@ import io.papermc.paper.plugin.provider.ProviderStatus;
import io.papermc.paper.plugin.provider.ProviderStatusHolder;
import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.TextComponent;
@@ -23,51 +28,38 @@ import org.bukkit.command.defaults.BukkitCommand;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class PaperPluginsCommand extends BukkitCommand {
private static final TextColor INFO_COLOR = TextColor.color(52, 159, 218);
// TODO: LINK?
private static final Component SERVER_PLUGIN_INFO = Component.text(" What is a server plugin?", INFO_COLOR)
.append(asPlainComponents("""
Server plugins can add new behavior to your server!
You can find new plugins on Paper's plugin repository, Hangar.
<link to hangar>
"""));
private static final Component SERVER_INITIALIZER_INFO = Component.text(" What is a server initializer?", INFO_COLOR)
.append(asPlainComponents("""
Server initializers are ran before your server
starts and are provided by paper plugins.
"""));
Server plugins can add new behavior to your server!
You can find new plugins on Paper's plugin repository, Hangar.
https://hangar.papermc.io/
"""));
private static final Component LEGACY_PLUGIN_INFO = Component.text(" What is a legacy plugin?", INFO_COLOR)
.append(asPlainComponents("""
A legacy plugin is a plugin that was made on
very old unsupported versions of the game.
It is encouraged that you replace this plugin,
as they might not work in the future and may cause
performance issues.
"""));
A legacy plugin is a plugin that was made on
very old unsupported versions of the game.
It is encouraged that you replace this plugin,
as they might not work in the future and may cause
performance issues.
"""));
private static final Component LEGACY_PLUGIN_STAR = Component.text('*', TextColor.color(255, 212, 42)).hoverEvent(LEGACY_PLUGIN_INFO);
private static final Component INFO_ICON_START = Component.text(" ", INFO_COLOR);
private static final Component PAPER_HEADER = Component.text("Paper Plugins:", TextColor.color(2, 136, 209));
private static final Component BUKKIT_HEADER = Component.text("Bukkit Plugins:", TextColor.color(237, 129, 6));
private static final Component PLUGIN_TICK = Component.text("- ", NamedTextColor.DARK_GRAY);
private static final Component PLUGIN_TICK_EMPTY = Component.text(" ");
private static final Component INFO_ICON_SERVER_PLUGIN = INFO_ICON_START.hoverEvent(SERVER_PLUGIN_INFO).clickEvent(ClickEvent.openUrl("https://docs.papermc.io/paper/adding-plugins"));
private static final Type JAVA_PLUGIN_PROVIDER_TYPE = new TypeToken<PluginProvider<JavaPlugin>>() {}.getType();
public PaperPluginsCommand() {
@@ -75,17 +67,17 @@ public class PaperPluginsCommand extends BukkitCommand {
this.description = "Gets a list of plugins running on the server";
this.usageMessage = "/plugins";
this.setPermission("bukkit.command.plugins");
this.setAliases(Arrays.asList("pl"));
this.setAliases(List.of("pl"));
}
private static <T> List<Component> formatProviders(TreeMap<String, PluginProvider<T>> plugins) {
List<Component> components = new ArrayList<>(plugins.size());
for (PluginProvider<T> entry : plugins.values()) {
private static <T> List<Component> formatProviders(final TreeMap<String, PluginProvider<T>> plugins) {
final List<Component> components = new ArrayList<>(plugins.size());
for (final PluginProvider<T> entry : plugins.values()) {
components.add(formatProvider(entry));
}
boolean isFirst = true;
List<Component> formattedSublists = new ArrayList<>();
final List<Component> formattedSubLists = new ArrayList<>();
/*
Split up the plugin list for each 10 plugins to get size down
@@ -93,30 +85,29 @@ public class PaperPluginsCommand extends BukkitCommand {
- Plugin 1, Plugin 2, .... Plugin 10,
Plugin 11, Plugin 12 ... Plugin 20,
*/
for (List<Component> componentSublist : Lists.partition(components, 10)) {
for (final List<Component> componentSublist : Lists.partition(components, 10)) {
Component component = Component.space();
if (isFirst) {
component = component.append(PLUGIN_TICK);
isFirst = false;
} else {
component = PLUGIN_TICK_EMPTY;
//formattedSublists.add(Component.empty()); // Add an empty line, the auto chat wrapping and this makes it quite jarring.
}
formattedSublists.add(component.append(Component.join(JoinConfiguration.commas(true), componentSublist)));
formattedSubLists.add(component.append(Component.join(JoinConfiguration.commas(true), componentSublist)));
}
return formattedSublists;
return formattedSubLists;
}
private static Component formatProvider(PluginProvider<?> provider) {
TextComponent.Builder builder = Component.text();
if (provider instanceof SpigotPluginProvider spigotPluginProvider && CraftMagicNumbers.isLegacy(spigotPluginProvider.getMeta())) {
private static Component formatProvider(final PluginProvider<?> provider) {
final TextComponent.Builder builder = Component.text();
if (provider instanceof final SpigotPluginProvider spigotPluginProvider && CraftMagicNumbers.isLegacy(spigotPluginProvider.getMeta())) {
builder.append(LEGACY_PLUGIN_STAR);
}
String name = provider.getMeta().getName();
Component pluginName = Component.text(name, fromStatus(provider))
final String name = provider.getMeta().getName();
final Component pluginName = Component.text(name, fromStatus(provider))
.clickEvent(ClickEvent.runCommand("/version " + name));
builder.append(pluginName);
@@ -124,9 +115,20 @@ public class PaperPluginsCommand extends BukkitCommand {
return builder.build();
}
private static Component asPlainComponents(String strings) {
net.kyori.adventure.text.TextComponent.Builder builder = Component.text();
for (String string : strings.split("\n")) {
private static Component header(final String header, final int color, final int count, final boolean showSize) {
final TextComponent.Builder componentHeader = Component.text().color(TextColor.color(color))
.append(Component.text(header));
if (showSize) {
componentHeader.appendSpace().append(Component.text("(" + count + ")"));
}
return componentHeader.append(Component.text(":")).build();
}
private static Component asPlainComponents(final String strings) {
final net.kyori.adventure.text.TextComponent.Builder builder = Component.text();
for (final String string : strings.split("\n")) {
builder.append(Component.newline());
builder.append(Component.text(string, NamedTextColor.WHITE));
}
@@ -134,13 +136,13 @@ public class PaperPluginsCommand extends BukkitCommand {
return builder.build();
}
private static TextColor fromStatus(PluginProvider<?> provider) {
if (provider instanceof ProviderStatusHolder statusHolder && statusHolder.getLastProvidedStatus() != null) {
ProviderStatus status = statusHolder.getLastProvidedStatus();
private static TextColor fromStatus(final PluginProvider<?> provider) {
if (provider instanceof final ProviderStatusHolder statusHolder && statusHolder.getLastProvidedStatus() != null) {
final ProviderStatus status = statusHolder.getLastProvidedStatus();
// Handle enabled/disabled game plugins
if (status == ProviderStatus.INITIALIZED && GenericTypeReflector.isSuperType(JAVA_PLUGIN_PROVIDER_TYPE, provider.getClass())) {
Plugin plugin = Bukkit.getPluginManager().getPlugin(provider.getMeta().getName());
final Plugin plugin = Bukkit.getPluginManager().getPlugin(provider.getMeta().getName());
// Plugin doesn't exist? Could be due to it being removed.
if (plugin == null) {
return NamedTextColor.RED;
@@ -153,7 +155,7 @@ public class PaperPluginsCommand extends BukkitCommand {
case INITIALIZED -> NamedTextColor.GREEN;
case ERRORED -> NamedTextColor.RED;
};
} else if (provider instanceof PaperPluginParent.PaperServerPluginProvider serverPluginProvider && serverPluginProvider.shouldSkipCreation()) {
} else if (provider instanceof final PaperPluginParent.PaperServerPluginProvider serverPluginProvider && serverPluginProvider.shouldSkipCreation()) {
// Paper plugins will be skipped if their provider is skipped due to their initializer failing.
// Show them as red
return NamedTextColor.RED;
@@ -165,15 +167,14 @@ public class PaperPluginsCommand extends BukkitCommand {
}
@Override
public boolean execute(@NotNull CommandSender sender, @NotNull String currentAlias, @NotNull String[] args) {
public boolean execute(final CommandSender sender, final String currentAlias, final String[] args) {
if (!this.testPermission(sender)) return true;
TreeMap<String, PluginProvider<JavaPlugin>> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
TreeMap<String, PluginProvider<JavaPlugin>> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
final TreeMap<String, PluginProvider<JavaPlugin>> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
final TreeMap<String, PluginProvider<JavaPlugin>> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (PluginProvider<JavaPlugin> provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) {
PluginMeta configuration = provider.getMeta();
for (final PluginProvider<JavaPlugin> provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) {
final PluginMeta configuration = provider.getMeta();
if (provider instanceof SpigotPluginProvider) {
spigotPlugins.put(configuration.getDisplayName(), provider);
@@ -182,34 +183,36 @@ public class PaperPluginsCommand extends BukkitCommand {
}
}
Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE);
//.append(INFO_ICON_START.hoverEvent(SERVER_PLUGIN_INFO)); TODO: Add docs
final int sizePaperPlugins = paperPlugins.size();
final int sizeSpigotPlugins = spigotPlugins.size();
final int sizePlugins = sizePaperPlugins + sizeSpigotPlugins;
final boolean hasAllPluginTypes = (sizePaperPlugins > 0 && sizeSpigotPlugins > 0);
final Component infoMessage = Component.text().append(INFO_ICON_SERVER_PLUGIN).append(Component.text("Server Plugins (%s):".formatted(sizePlugins), NamedTextColor.WHITE)).build();
sender.sendMessage(infoMessage);
if (!paperPlugins.isEmpty()) {
sender.sendMessage(PAPER_HEADER);
sender.sendMessage(header("Paper Plugins", 0x0288D1, sizePaperPlugins, hasAllPluginTypes));
}
for (Component component : formatProviders(paperPlugins)) {
for (final Component component : formatProviders(paperPlugins)) {
sender.sendMessage(component);
}
if (!spigotPlugins.isEmpty()) {
sender.sendMessage(BUKKIT_HEADER);
sender.sendMessage(header("Bukkit Plugins", 0xED8106, sizeSpigotPlugins, hasAllPluginTypes));
}
for (Component component : formatProviders(spigotPlugins)) {
for (final Component component : formatProviders(spigotPlugins)) {
sender.sendMessage(component);
}
return true;
}
@NotNull
@Override
public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException {
public List<String> tabComplete(final CommandSender sender, final String alias, final String[] args) throws IllegalArgumentException {
return Collections.emptyList();
}
}

View File

@@ -124,7 +124,7 @@ public abstract class ApiMirrorRootNode extends RootCommandNode<CommandSourceSta
}
converted = this.unwrapArgumentWrapper(pureArgumentNode, customArgumentType, customArgumentType.getNativeType(), suggestionProvider);
} else if (pureArgumentType instanceof final VanillaArgumentProviderImpl.NativeWrapperArgumentType<?,?> nativeWrapperArgumentType) {
} else if (pureArgumentType instanceof final VanillaArgumentProviderImpl.NativeWrapperArgumentType<?, ?> nativeWrapperArgumentType) {
converted = this.unwrapArgumentWrapper(pureArgumentNode, nativeWrapperArgumentType, nativeWrapperArgumentType, null); // "null" for suggestion provider so it uses the argument type's suggestion provider
/*
@@ -140,6 +140,8 @@ public abstract class ApiMirrorRootNode extends RootCommandNode<CommandSourceSta
// Unknown argument type was passed
throw new IllegalArgumentException("Custom unknown argument type was passed, should be wrapped inside an CustomArgumentType.");
}
} else if (pureNode == this) {
return (CommandNode) this.getDispatcher().getRoot();
} else {
throw new IllegalArgumentException("Unknown command node passed. Don't know how to unwrap this.");
}

View File

@@ -1,24 +1,19 @@
package io.papermc.paper.command.brigadier;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import io.papermc.paper.command.brigadier.bukkit.BukkitBrigForwardingMap;
import io.papermc.paper.command.brigadier.bukkit.BukkitCommandNode;
import java.util.Map;
import net.minecraft.commands.CommandSource;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandMap;
import org.bukkit.craftbukkit.command.VanillaCommandWrapper;
import java.util.Map;
public final class PaperBrigadier {
@SuppressWarnings("DataFlowIssue")
@@ -40,7 +35,8 @@ public final class PaperBrigadier {
throw new IllegalArgumentException("Unsure how to wrap a " + node);
}
if (!(node instanceof PluginCommandNode pluginCommandNode)) {
final PluginCommandMeta meta;
if ((meta = node.pluginCommandMeta) == null) {
return new VanillaCommandWrapper(null, node);
}
CommandNode<CommandSourceStack> argumentCommandNode = node;
@@ -49,8 +45,8 @@ public final class PaperBrigadier {
}
Map<CommandNode<CommandSourceStack>, String> map = PaperCommands.INSTANCE.getDispatcherInternal().getSmartUsage(argumentCommandNode, DUMMY);
String usage = map.isEmpty() ? pluginCommandNode.getUsageText() : pluginCommandNode.getUsageText() + " " + String.join("\n" + pluginCommandNode.getUsageText() + " ", map.values());
return new PluginVanillaCommandWrapper(pluginCommandNode.getName(), pluginCommandNode.getDescription(), usage, pluginCommandNode.getAliases(), node, pluginCommandNode.getPlugin());
String usage = map.isEmpty() ? node.getUsageText() : node.getUsageText() + " " + String.join("\n" + node.getUsageText() + " ", map.values());
return new PluginVanillaCommandWrapper(node.getName(), meta.description(), usage, meta.aliases(), node, meta.plugin());
}
/*
@@ -70,4 +66,19 @@ public final class PaperBrigadier {
}
}
}
public static <S> LiteralCommandNode<S> copyLiteral(final String newLiteral, final LiteralCommandNode<S> source) {
// logic copied from LiteralCommandNode#createBuilder
final LiteralArgumentBuilder<S> copyBuilder = LiteralArgumentBuilder.<S>literal(newLiteral)
.requires(source.getRequirement())
.forward(source.getRedirect(), source.getRedirectModifier(), source.isFork());
if (source.getCommand() != null) {
copyBuilder.executes(source.getCommand());
}
for (final CommandNode<S> child : source.getChildren()) {
copyBuilder.then(child);
}
return copyBuilder.build();
}
}

View File

@@ -1,21 +1,23 @@
package io.papermc.paper.command.brigadier;
import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource;
import com.google.common.base.Preconditions;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.craftbukkit.entity.CraftEntity;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
public interface PaperCommandSourceStack extends CommandSourceStack, BukkitBrigadierCommandSource {
net.minecraft.commands.CommandSourceStack getHandle();
@Override
default @NotNull Location getLocation() {
default @NonNull Location getLocation() {
Vec2 rot = this.getHandle().getRotation();
Vec3 pos = this.getHandle().getPosition();
Level level = this.getHandle().getLevel();
@@ -24,7 +26,7 @@ public interface PaperCommandSourceStack extends CommandSourceStack, BukkitBriga
}
@Override
@NotNull
@NonNull
default CommandSender getSender() {
return this.getHandle().getBukkitSender();
}
@@ -40,6 +42,12 @@ public interface PaperCommandSourceStack extends CommandSourceStack, BukkitBriga
return nmsEntity.getBukkitEntity();
}
@Override
default CommandSourceStack withExecutor(@NonNull Entity executor) {
Preconditions.checkNotNull(executor, "Executor cannot be null.");
return this.getHandle().withEntity(((CraftEntity) executor).getHandle());
}
// OLD METHODS
@Override
default org.bukkit.entity.Entity getBukkitEntity() {

View File

@@ -21,23 +21,20 @@ import java.util.Set;
import net.minecraft.commands.CommandBuildContext;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import static java.util.Objects.requireNonNull;
@DefaultQualifier(NonNull.class)
@NullMarked
public class PaperCommands implements Commands, PaperRegistrar<LifecycleEventOwner> {
public static final PaperCommands INSTANCE = new PaperCommands();
private @Nullable LifecycleEventOwner currentContext;
private @MonotonicNonNull CommandDispatcher<CommandSourceStack> dispatcher;
private @MonotonicNonNull CommandBuildContext buildContext;
private @Nullable CommandDispatcher<CommandSourceStack> dispatcher;
private @Nullable CommandBuildContext buildContext;
private boolean invalid = false;
@Override
@@ -93,65 +90,48 @@ public class PaperCommands implements Commands, PaperRegistrar<LifecycleEventOwn
}
@Override
public @Unmodifiable Set<String> registerWithFlags(@NotNull final PluginMeta pluginMeta, @NotNull final LiteralCommandNode<CommandSourceStack> node, @org.jetbrains.annotations.Nullable final String description, @NotNull final Collection<String> aliases, @NotNull final Set<CommandRegistrationFlag> flags) {
final boolean hasFlattenRedirectFlag = flags.contains(CommandRegistrationFlag.FLATTEN_ALIASES);
public @Unmodifiable Set<String> registerWithFlags(final PluginMeta pluginMeta, final LiteralCommandNode<CommandSourceStack> node, final @Nullable String description, final Collection<String> aliases, final Set<CommandRegistrationFlag> flags) {
final PluginCommandMeta meta = new PluginCommandMeta(pluginMeta, description);
final String identifier = pluginMeta.getName().toLowerCase(Locale.ROOT);
final String literal = node.getLiteral();
final PluginCommandNode pluginLiteral = new PluginCommandNode(identifier + ":" + literal, pluginMeta, node, description); // Treat the keyed version of the command as the root
final LiteralCommandNode<CommandSourceStack> pluginLiteral = PaperBrigadier.copyLiteral(identifier + ":" + literal, node);
final Set<String> registeredLabels = new HashSet<>(aliases.size() * 2 + 2);
if (this.registerIntoDispatcher(pluginLiteral, true)) {
registeredLabels.add(pluginLiteral.getLiteral());
}
if (this.registerRedirect(literal, pluginMeta, pluginLiteral, description, true, hasFlattenRedirectFlag)) { // Plugin commands should override vanilla commands
if (this.registerIntoDispatcher(node, true)) { // Plugin commands should override vanilla commands
registeredLabels.add(literal);
}
// Add aliases
final List<String> registeredAliases = new ArrayList<>(aliases.size() * 2);
for (final String alias : aliases) {
if (this.registerRedirect(alias, pluginMeta, pluginLiteral, description, false, hasFlattenRedirectFlag)) {
if (this.registerCopy(alias, pluginLiteral, meta)) {
registeredAliases.add(alias);
}
if (this.registerRedirect(identifier + ":" + alias, pluginMeta, pluginLiteral, description, false, hasFlattenRedirectFlag)) {
if (this.registerCopy(identifier + ":" + alias, pluginLiteral, meta)) {
registeredAliases.add(identifier + ":" + alias);
}
}
if (!registeredAliases.isEmpty()) {
pluginLiteral.setAliases(registeredAliases);
}
pluginLiteral.pluginCommandMeta = new PluginCommandMeta(pluginMeta, description, registeredAliases);
node.pluginCommandMeta = pluginLiteral.pluginCommandMeta;
registeredLabels.addAll(registeredAliases);
return registeredLabels.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(registeredLabels);
}
private boolean registerRedirect(final String aliasLiteral, final PluginMeta plugin, final PluginCommandNode redirectTo, final @Nullable String description, final boolean override, boolean hasFlattenRedirectFlag) {
final LiteralCommandNode<CommandSourceStack> redirect;
if (redirectTo.getChildren().isEmpty() || hasFlattenRedirectFlag) {
redirect = Commands.literal(aliasLiteral)
.executes(redirectTo.getCommand())
.requires(redirectTo.getRequirement())
.build();
for (final CommandNode<CommandSourceStack> child : redirectTo.getChildren()) {
redirect.addChild(child);
}
} else {
redirect = Commands.literal(aliasLiteral)
.executes(redirectTo.getCommand())
.redirect(redirectTo)
.requires(redirectTo.getRequirement())
.build();
}
return this.registerIntoDispatcher(new PluginCommandNode(aliasLiteral, plugin, redirect, description), override);
private boolean registerCopy(final String aliasLiteral, final LiteralCommandNode<CommandSourceStack> redirectTo, final PluginCommandMeta meta) {
final LiteralCommandNode<CommandSourceStack> node = PaperBrigadier.copyLiteral(aliasLiteral, redirectTo);
node.pluginCommandMeta = meta;
return this.registerIntoDispatcher(node, false);
}
private boolean registerIntoDispatcher(final PluginCommandNode node, boolean override) {
final @Nullable CommandNode<CommandSourceStack> existingChild = this.getDispatcher().getRoot().getChild(node.getLiteral());
if (existingChild != null && !(existingChild instanceof PluginCommandNode) && !(existingChild instanceof BukkitCommandNode)) {
private boolean registerIntoDispatcher(final LiteralCommandNode<CommandSourceStack> node, boolean override) {
final CommandNode<CommandSourceStack> existingChild = this.getDispatcher().getRoot().getChild(node.getLiteral());
if (existingChild != null && existingChild.pluginCommandMeta == null && !(existingChild instanceof BukkitCommandNode)) {
override = true; // override vanilla commands
}
if (existingChild == null || override) { // Avoid merging behavior. Maybe something to look into in the future

View File

@@ -0,0 +1,26 @@
package io.papermc.paper.command.brigadier;
import io.papermc.paper.plugin.configuration.PluginMeta;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public record PluginCommandMeta(PluginMeta pluginMeta, @Nullable String description, List<String> aliases) {
public PluginCommandMeta(final PluginMeta pluginMeta, final @Nullable String description) {
this(pluginMeta, description, Collections.emptyList());
}
public PluginCommandMeta {
aliases = List.copyOf(aliases);
}
public Plugin plugin() {
return Objects.requireNonNull(Bukkit.getPluginManager().getPlugin(this.pluginMeta.getName()));
}
}

View File

@@ -1,50 +0,0 @@
package io.papermc.paper.command.brigadier;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import io.papermc.paper.plugin.configuration.PluginMeta;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PluginCommandNode extends LiteralCommandNode<CommandSourceStack> {
private final PluginMeta plugin;
private final String description;
private List<String> aliases = Collections.emptyList();
public PluginCommandNode(final @NotNull String literal, final @NotNull PluginMeta plugin, final @NotNull LiteralCommandNode<CommandSourceStack> rootLiteral, final @Nullable String description) {
super(
literal, rootLiteral.getCommand(), rootLiteral.getRequirement(),
rootLiteral.getRedirect(), rootLiteral.getRedirectModifier(), rootLiteral.isFork()
);
this.plugin = plugin;
this.description = description;
for (CommandNode<CommandSourceStack> argument : rootLiteral.getChildren()) {
this.addChild(argument);
}
}
@NotNull
public Plugin getPlugin() {
return Objects.requireNonNull(Bukkit.getPluginManager().getPlugin(this.plugin.getName()));
}
@NotNull
public String getDescription() {
return this.description;
}
public void setAliases(List<String> aliases) {
this.aliases = aliases;
}
public List<String> getAliases() {
return this.aliases;
}
}

View File

@@ -19,9 +19,11 @@ import io.papermc.paper.command.brigadier.argument.range.RangeProvider;
import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver;
import io.papermc.paper.command.brigadier.argument.resolvers.FinePositionResolver;
import io.papermc.paper.command.brigadier.argument.resolvers.PlayerProfileListResolver;
import io.papermc.paper.command.brigadier.argument.resolvers.RotationResolver;
import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver;
import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver;
import io.papermc.paper.entity.LookAnchor;
import io.papermc.paper.math.Rotation;
import io.papermc.paper.registry.PaperRegistries;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
@@ -61,6 +63,7 @@ import net.minecraft.commands.arguments.TimeArgument;
import net.minecraft.commands.arguments.UuidArgument;
import net.minecraft.commands.arguments.blocks.BlockStateArgument;
import net.minecraft.commands.arguments.coordinates.BlockPosArgument;
import net.minecraft.commands.arguments.coordinates.RotationArgument;
import net.minecraft.commands.arguments.coordinates.Vec3Argument;
import net.minecraft.commands.arguments.item.ItemArgument;
import net.minecraft.commands.arguments.item.ItemPredicateArgument;
@@ -71,6 +74,7 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import org.bukkit.GameMode;
import org.bukkit.HeightMap;
@@ -156,6 +160,15 @@ public class VanillaArgumentProviderImpl implements VanillaArgumentProvider {
});
}
@Override
public ArgumentType<RotationResolver> rotation() {
return this.wrap(RotationArgument.rotation(), (result) -> sourceStack -> {
final Vec2 vec2 = result.getRotation((CommandSourceStack) sourceStack);
return Rotation.rotation(vec2.y, vec2.x);
});
}
@Override
public ArgumentType<BlockState> blockState() {
return this.wrap(BlockStateArgument.block(PaperCommands.INSTANCE.getBuildContext()), (result) -> {
@@ -353,6 +366,11 @@ public class VanillaArgumentProviderImpl implements VanillaArgumentProvider {
return this.converter.convert(this.nmsBase.parse(reader));
}
@Override
public <S> P parse(final StringReader reader, final S source) throws CommandSyntaxException {
return this.converter.convert(this.nmsBase.parse(reader, source));
}
@Override
public <S> CompletableFuture<Suggestions> listSuggestions(final CommandContext<S> context, final SuggestionsBuilder builder) {
return this.nmsBase.listSuggestions(context, builder);

View File

@@ -46,7 +46,7 @@ public class WrappedArgumentCommandNode<NMS, API> extends ArgumentCommandNode<Co
@Override
public void parse(final StringReader reader, final CommandContextBuilder<CommandSourceStack> contextBuilder) throws CommandSyntaxException {
final int start = reader.getCursor();
final API result = this.pureArgumentType.parse(reader); // Use the api argument parser
final API result = this.pureArgumentType.parse(reader, contextBuilder.getSource()); // Use the api argument parser
final ParsedArgument<CommandSourceStack, API> parsed = new ParsedArgument<>(start, reader.getCursor(), result); // Return an API parsed argument instead.
contextBuilder.withArgument(this.getName(), parsed);

View File

@@ -80,7 +80,7 @@ public abstract class Configurations<G, W> {
}
@MustBeInvokedByOverriders
protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder() {
protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder(RegistryAccess registryAccess) {
return this.createLoaderBuilder();
}
@@ -104,7 +104,7 @@ public abstract class Configurations<G, W> {
}
public G initializeGlobalConfiguration(final RegistryAccess registryAccess) throws ConfigurateException {
return this.initializeGlobalConfiguration(creator(this.globalConfigClass, true));
return this.initializeGlobalConfiguration(registryAccess, creator(this.globalConfigClass, true));
}
private void trySaveFileNode(YamlConfigurationLoader loader, ConfigurationNode node, String filename) throws ConfigurateException {
@@ -117,9 +117,9 @@ public abstract class Configurations<G, W> {
}
}
protected G initializeGlobalConfiguration(final CheckedFunction<ConfigurationNode, G, SerializationException> creator) throws ConfigurateException {
protected G initializeGlobalConfiguration(final RegistryAccess registryAccess, final CheckedFunction<ConfigurationNode, G, SerializationException> creator) throws ConfigurateException {
final Path configFile = this.globalFolder.resolve(this.globalConfigFileName);
final YamlConfigurationLoader loader = this.createGlobalLoaderBuilder()
final YamlConfigurationLoader loader = this.createGlobalLoaderBuilder(registryAccess)
.defaultOptions(this.applyObjectMapperFactory(this.createGlobalObjectMapperFactoryBuilder().build()))
.path(configFile)
.build();

View File

@@ -5,10 +5,13 @@ import io.papermc.paper.FeatureHooks;
import io.papermc.paper.configuration.constraint.Constraints;
import io.papermc.paper.configuration.type.number.DoubleOr;
import io.papermc.paper.configuration.type.number.IntOr;
import io.papermc.paper.util.ItemObfuscationBinding;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
@@ -20,6 +23,7 @@ import org.spongepowered.configurate.objectmapping.meta.Setting;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.Set;
@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"})
public class GlobalConfiguration extends ConfigurationPart {
@@ -69,7 +73,7 @@ public class GlobalConfiguration extends ConfigurationPart {
)
public int playerMaxConcurrentChunkGenerates = 0;
}
static void set(GlobalConfiguration instance) {
static void set(final GlobalConfiguration instance) {
GlobalConfiguration.instance = instance;
}
@@ -166,6 +170,8 @@ public class GlobalConfiguration extends ConfigurationPart {
public class UnsupportedSettings extends ConfigurationPart {
@Comment("This setting allows for exploits related to end portals, for example sand duping")
public boolean allowUnsafeEndPortalTeleportation = false;
@Comment("This setting controls the ability to enable dupes related to tripwires.")
public boolean skipTripwireHookPlacementValidation = false;
@Comment("This setting controls if players should be able to break bedrock, end portals and other intended to be permanent blocks.")
public boolean allowPermanentBlockBreakExploits = false;
@Comment("This setting controls if player should be able to use TNT duplication, but this also allows duplicating carpet, rails and potentially other items")
@@ -177,10 +183,15 @@ public class GlobalConfiguration extends ConfigurationPart {
public boolean skipVanillaDamageTickWhenShieldBlocked = false;
@Comment("This setting controls what compression format is used for region files.")
public CompressionFormat compressionFormat = CompressionFormat.ZLIB;
@Comment("This setting controls if equipment should be updated when handling certain player actions.")
public boolean updateEquipmentOnPlayerActions = true;
@Comment("Only checks an item's amount and type instead of its full data during inventory desync checks.")
public boolean simplifyRemoteItemMatching = false;
public enum CompressionFormat {
GZIP,
ZLIB,
LZ4,
NONE
}
}
@@ -189,8 +200,9 @@ public class GlobalConfiguration extends ConfigurationPart {
public class Commands extends ConfigurationPart {
public boolean suggestPlayerNamesWhenNullTabCompletions = true;
public boolean fixTargetSelectorTagCompletion = true;
public boolean timeCommandAffectsAllWorlds = false;
@Comment("Allow mounting entities to a player in the Vanilla '/ride' command.")
public boolean rideCommandAllowPlayerAsVehicle = false;
}
public Logging logging;
@@ -354,4 +366,41 @@ public class GlobalConfiguration extends ConfigurationPart {
public boolean disableChorusPlantUpdates = false;
public boolean disableMushroomBlockUpdates = false;
}
public Anticheat anticheat;
public class Anticheat extends ConfigurationPart {
public Obfuscation obfuscation;
public class Obfuscation extends ConfigurationPart {
public Items items;
public class Items extends ConfigurationPart {
public boolean enableItemObfuscation = false;
public ItemObfuscationBinding.AssetObfuscationConfiguration allModels = new ItemObfuscationBinding.AssetObfuscationConfiguration(
true,
Set.of(DataComponents.LODESTONE_TRACKER),
Set.of()
);
public Map<ResourceLocation, ItemObfuscationBinding.AssetObfuscationConfiguration> modelOverrides = Map.of(
Objects.requireNonNull(net.minecraft.world.item.Items.ELYTRA.components().get(DataComponents.ITEM_MODEL)),
new ItemObfuscationBinding.AssetObfuscationConfiguration(
true,
Set.of(DataComponents.DAMAGE),
Set.of()
)
);
public transient ItemObfuscationBinding binding;
@PostProcess
public void bindDataSanitizer() {
this.binding = new ItemObfuscationBinding(this);
}
}
}
}
}

View File

@@ -10,6 +10,7 @@ import io.papermc.paper.configuration.serializer.ComponentSerializer;
import io.papermc.paper.configuration.serializer.EnumValueSerializer;
import io.papermc.paper.configuration.serializer.NbtPathSerializer;
import io.papermc.paper.configuration.serializer.PacketClassSerializer;
import io.papermc.paper.configuration.serializer.ResourceLocationSerializer;
import io.papermc.paper.configuration.serializer.StringRepresentableSerializer;
import io.papermc.paper.configuration.serializer.collections.FastutilMapSerializer;
import io.papermc.paper.configuration.serializer.collections.MapSerializer;
@@ -48,6 +49,7 @@ import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
@@ -180,6 +182,7 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
.register(Duration.SERIALIZER)
.register(DurationOrDisabled.SERIALIZER)
.register(NbtPathSerializer.SERIALIZER)
.register(ResourceLocationSerializer.INSTANCE)
);
}
@@ -193,16 +196,17 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
}
@Override
protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder() {
return super.createGlobalLoaderBuilder()
.defaultOptions(PaperConfigurations::defaultGlobalOptions);
protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder(RegistryAccess registryAccess) {
return super.createGlobalLoaderBuilder(registryAccess)
.defaultOptions((options) -> defaultGlobalOptions(registryAccess, options));
}
private static ConfigurationOptions defaultGlobalOptions(ConfigurationOptions options) {
private static ConfigurationOptions defaultGlobalOptions(RegistryAccess registryAccess, ConfigurationOptions options) {
return options
.header(GLOBAL_HEADER)
.serializers(builder -> builder
.register(new PacketClassSerializer())
.register(new RegistryValueSerializer<>(new TypeToken<DataComponentType<?>>() {}, registryAccess, Registries.DATA_COMPONENT_TYPE, false))
);
}
@@ -316,7 +320,7 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
public void reloadConfigs(MinecraftServer server) {
try {
this.initializeGlobalConfiguration(reloader(this.globalConfigClass, GlobalConfiguration.get()));
this.initializeGlobalConfiguration(server.registryAccess(), reloader(this.globalConfigClass, GlobalConfiguration.get()));
this.initializeWorldDefaultsConfiguration(server.registryAccess());
for (ServerLevel level : server.getAllLevels()) {
this.createWorldConfig(createWorldContextMap(level), reloader(this.worldConfigClass, level.paperConfig()));
@@ -454,9 +458,9 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
}
@VisibleForTesting
static ConfigurationNode createForTesting() {
static ConfigurationNode createForTesting(RegistryAccess registryAccess) {
ObjectMapper.Factory factory = defaultGlobalFactoryBuilder(ObjectMapper.factoryBuilder()).build();
ConfigurationOptions options = defaultGlobalOptions(defaultOptions(ConfigurationOptions.defaults()))
ConfigurationOptions options = defaultGlobalOptions(registryAccess, defaultOptions(ConfigurationOptions.defaults()))
.serializers(builder -> builder.register(type -> ConfigurationPart.class.isAssignableFrom(erase(type)), factory.asTypeSerializer()));
return BasicConfigurationNode.root(options);
}

View File

@@ -54,7 +54,8 @@ interface RemovedConfigurations {
path("fixes", "fix-curing-zombie-villager-discount-exploit"),
path("entities", "mob-effects", "undead-immune-to-certain-effects"),
path("entities", "entities-target-with-follow-range"),
path("environment", "disable-teleportation-suffocation-check")
path("environment", "disable-teleportation-suffocation-check"),
path("misc", "light-queue-size")
};
// spawn.keep-spawn-loaded and spawn.keep-spawn-loaded-range are no longer used, but kept
// in the world default config for compatibility with old worlds being migrated to use the gamerule
@@ -79,7 +80,8 @@ interface RemovedConfigurations {
path("warnWhenSettingExcessiveVelocity"),
path("logging", "use-rgb-for-named-text-colors"),
path("unsupported-settings", "allow-grindstone-overstacking"),
path("unsupported-settings", "allow-tripwire-disarming-exploits")
path("unsupported-settings", "allow-tripwire-disarming-exploits"),
path("commands", "fix-target-selector-tag-completion"),
};
}

View File

@@ -88,17 +88,6 @@ public class WorldConfiguration extends ConfigurationPart {
public class Anticheat extends ConfigurationPart {
public Obfuscation obfuscation;
public class Obfuscation extends ConfigurationPart {
public Items items = new Items();
public class Items extends ConfigurationPart {
public boolean hideItemmeta = false;
public boolean hideDurability = false;
public boolean hideItemmetaWithVisualEffects = false;
}
}
public AntiXray antiXray;
public class AntiXray extends ConfigurationPart {
@@ -338,6 +327,9 @@ public class WorldConfiguration extends ConfigurationPart {
public int day = 5;
}
}
@Comment("Adds a cooldown to bees being released after a failed release, which can occur if the hive is blocked or it being night.")
public boolean cooldownFailedBeehiveReleases = true;
}
public TrackingRangeY trackingRangeY;
@@ -566,7 +558,6 @@ public class WorldConfiguration extends ConfigurationPart {
public Misc misc;
public class Misc extends ConfigurationPart {
public int lightQueueSize = 20;
public boolean updatePathfindingOnBlockUpdate = true;
public boolean showSignClickCommandFailureMsgsToPlayer = false;
public RedstoneImplementation redstoneImplementation = RedstoneImplementation.VANILLA;

View File

@@ -0,0 +1,26 @@
package io.papermc.paper.configuration.serializer;
import java.lang.reflect.Type;
import java.util.function.Predicate;
import net.minecraft.resources.ResourceLocation;
import org.spongepowered.configurate.serialize.ScalarSerializer;
import org.spongepowered.configurate.serialize.SerializationException;
public class ResourceLocationSerializer extends ScalarSerializer<ResourceLocation> {
public static final ScalarSerializer<ResourceLocation> INSTANCE = new ResourceLocationSerializer();
private ResourceLocationSerializer() {
super(ResourceLocation.class);
}
@Override
public ResourceLocation deserialize(final Type type, final Object obj) throws SerializationException {
return ResourceLocation.read(obj.toString()).getOrThrow(s -> new SerializationException(ResourceLocation.class, s));
}
@Override
protected Object serialize(final ResourceLocation item, final Predicate<Class<?>> typeSupported) {
return item.toString();
}
}

View File

@@ -15,10 +15,18 @@ public record DataComponentAdapter<NMS, API>(
) {
static final Function<Void, Unit> API_TO_UNIT_CONVERTER = $ -> Unit.INSTANCE;
static final Function API_TO_UNIMPLEMENTED_CONVERTER = $ -> {
throw new UnsupportedOperationException("Cannot convert an API value to an unimplemented type");
};
public boolean isValued() {
return this.apiToVanilla != API_TO_UNIT_CONVERTER;
}
public boolean isUnimplemented() {
return this.apiToVanilla == API_TO_UNIMPLEMENTED_CONVERTER;
}
public NMS toVanilla(final API value) {
final NMS nms = this.apiToVanilla.apply(value);
if (this.codecValidation) {

View File

@@ -63,6 +63,10 @@ public final class DataComponentAdapters {
throw new UnsupportedOperationException("Cannot convert the Unit type to an API value");
};
static final Function UNIMPLEMENTED_TO_API_CONVERTER = $ -> {
throw new UnsupportedOperationException("Cannot convert the an unimplemented type to an API value");
};
static final Map<ResourceKey<DataComponentType<?>>, DataComponentAdapter<?, ?>> ADAPTERS = new HashMap<>();
public static void bootstrap() {
@@ -136,10 +140,9 @@ public final class DataComponentAdapters {
// register(DataComponents.LOCK, PaperLockCode::new);
register(DataComponents.CONTAINER_LOOT, PaperSeededContainerLoot::new);
// TODO: REMOVE THIS... we want to build the PR... so lets just make things UNTYPED!
for (final Map.Entry<ResourceKey<DataComponentType<?>>, DataComponentType<?>> componentType : BuiltInRegistries.DATA_COMPONENT_TYPE.entrySet()) {
if (!ADAPTERS.containsKey(componentType.getKey())) {
registerUntyped((DataComponentType<Unit>) componentType.getValue());
registerUnimplemented(componentType.getValue());
}
}
}
@@ -152,6 +155,10 @@ public final class DataComponentAdapters {
registerInternal(type, Function.identity(), Function.identity(), true);
}
public static <NMS> void registerUnimplemented(final DataComponentType<NMS> type) {
registerInternal(type, UNIMPLEMENTED_TO_API_CONVERTER, DataComponentAdapter.API_TO_UNIMPLEMENTED_CONVERTER, false);
}
private static <NMS, API extends Handleable<NMS>> void register(final DataComponentType<NMS> type, final Function<NMS, API> vanillaToApi) {
registerInternal(type, vanillaToApi, Handleable::getHandle, false);
}

View File

@@ -7,7 +7,6 @@ import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.craftbukkit.CraftRegistry;
import org.bukkit.craftbukkit.util.Handleable;
import org.jspecify.annotations.Nullable;
@@ -23,7 +22,7 @@ public abstract class PaperDataComponentType<T, NMS> implements DataComponentTyp
}
public static DataComponentType minecraftToBukkit(final net.minecraft.core.component.DataComponentType<?> type) {
return CraftRegistry.minecraftToBukkit(type, Registries.DATA_COMPONENT_TYPE, Registry.DATA_COMPONENT_TYPE);
return CraftRegistry.minecraftToBukkit(type, Registries.DATA_COMPONENT_TYPE);
}
public static Set<DataComponentType> minecraftToBukkit(final Set<net.minecraft.core.component.DataComponentType<?>> nmsTypes) {
@@ -78,7 +77,9 @@ public abstract class PaperDataComponentType<T, NMS> implements DataComponentTyp
if (adapter == null) {
throw new IllegalArgumentException("No adapter found for " + key);
}
if (adapter.isValued()) {
if (adapter.isUnimplemented()) {
return new Unimplemented<>(key, type, adapter);
} else if (adapter.isValued()) {
return new ValuedImpl<>(key, type, adapter);
} else {
return new NonValuedImpl<>(key, type, adapter);
@@ -106,4 +107,15 @@ public abstract class PaperDataComponentType<T, NMS> implements DataComponentTyp
super(key, type, adapter);
}
}
public static final class Unimplemented<T, NMS> extends PaperDataComponentType<T, NMS> {
public Unimplemented(
final NamespacedKey key,
final net.minecraft.core.component.DataComponentType<NMS> type,
final DataComponentAdapter<NMS, T> adapter
) {
super(key, type, adapter);
}
}
}

View File

@@ -1,6 +1,5 @@
package io.papermc.paper.datacomponent.item;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.util.MCUtil;
import java.util.List;
@@ -20,7 +19,7 @@ public record PaperBannerPatternLayers(
private static List<Pattern> convert(final net.minecraft.world.level.block.entity.BannerPatternLayers nmsPatterns) {
return MCUtil.transformUnmodifiable(nmsPatterns.layers(), input -> {
final Optional<PatternType> type = CraftRegistry.unwrapAndConvertHolder(RegistryAccess.registryAccess().getRegistry(RegistryKey.BANNER_PATTERN), input.pattern());
final Optional<PatternType> type = CraftRegistry.unwrapAndConvertHolder(RegistryKey.BANNER_PATTERN, input.pattern());
return new Pattern(Objects.requireNonNull(DyeColor.getByWoolData((byte) input.color().getId())), type.orElseThrow(() -> new IllegalStateException("Inlined banner patterns are not supported yet in the API!")));
});
}

View File

@@ -40,7 +40,7 @@ public record PaperCustomModelData(
@Override
public List<Color> colors() {
return MCUtil.transformUnmodifiable(this.impl.colors(), Color::fromRGB);
return MCUtil.transformUnmodifiable(this.impl.colors(), color -> Color.fromRGB(color & 0x00FFFFFF)); // skip alpha channel
}
static final class BuilderImpl implements CustomModelData.Builder {

View File

@@ -64,6 +64,6 @@ public class PaperDiscoveredDatapack implements DiscoveredDatapack {
@Override
public DatapackSource getSource() {
return PACK_SOURCES.computeIfAbsent(this.pack.location().source(), source -> new DatapackSourceImpl(source.toString()));
return PACK_SOURCES.get(this.pack.location().source());
}
}

View File

@@ -0,0 +1,20 @@
package io.papermc.paper.entity;
import java.util.Collection;
import java.util.Collections;
import org.bukkit.entity.Item;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.Unmodifiable;
import org.jspecify.annotations.NullMarked;
@NullMarked
public record PaperPlayerGiveResult(
@Unmodifiable Collection<ItemStack> leftovers,
@Unmodifiable Collection<Item> drops
) implements PlayerGiveResult {
public static final PlayerGiveResult EMPTY = new PaperPlayerGiveResult(
Collections.emptyList(), Collections.emptyList()
);
}

View File

@@ -4,6 +4,7 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.FlyingMob;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ambient.AmbientCreature;
import net.minecraft.world.entity.animal.AgeableWaterCreature;
import net.minecraft.world.entity.animal.WaterAnimal;
import net.minecraft.world.entity.monster.Enemy;
import net.minecraft.world.entity.npc.Villager;
@@ -28,7 +29,7 @@ public enum ActivationType {
* @return activation type
*/
public static ActivationType activationTypeFor(final Entity entity) {
if (entity instanceof WaterAnimal) {
if (entity instanceof WaterAnimal || entity instanceof AgeableWaterCreature) {
return ActivationType.WATER;
} else if (entity instanceof Villager) {
return ActivationType.VILLAGER;

View File

@@ -0,0 +1,90 @@
package io.papermc.paper.raytracing;
import com.google.common.base.Preconditions;
import java.util.EnumSet;
import java.util.OptionalDouble;
import java.util.function.Predicate;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.util.Vector;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class PositionedRayTraceConfigurationBuilderImpl implements PositionedRayTraceConfigurationBuilder {
public @Nullable Location start;
public @Nullable Vector direction;
public OptionalDouble maxDistance = OptionalDouble.empty();
public FluidCollisionMode fluidCollisionMode = FluidCollisionMode.NEVER;
public boolean ignorePassableBlocks;
public double raySize = 0.0D;
public @Nullable Predicate<? super Entity> entityFilter;
public @Nullable Predicate<? super Block> blockFilter;
public EnumSet<RayTraceTarget> targets = EnumSet.noneOf(RayTraceTarget.class);
@Override
public PositionedRayTraceConfigurationBuilder start(final Location start) {
Preconditions.checkArgument(start != null, "start must not be null");
this.start = start.clone();
return this;
}
@Override
public PositionedRayTraceConfigurationBuilder direction(final Vector direction) {
Preconditions.checkArgument(direction != null, "direction must not be null");
this.direction = direction.clone();
return this;
}
@Override
public PositionedRayTraceConfigurationBuilder maxDistance(final double maxDistance) {
Preconditions.checkArgument(maxDistance >= 0, "maxDistance must be non-negative");
this.maxDistance = OptionalDouble.of(maxDistance);
return this;
}
@Override
public PositionedRayTraceConfigurationBuilder fluidCollisionMode(final FluidCollisionMode fluidCollisionMode) {
Preconditions.checkArgument(fluidCollisionMode != null, "fluidCollisionMode must not be null");
this.fluidCollisionMode = fluidCollisionMode;
return this;
}
@Override
public PositionedRayTraceConfigurationBuilder ignorePassableBlocks(final boolean ignorePassableBlocks) {
this.ignorePassableBlocks = ignorePassableBlocks;
return this;
}
@Override
public PositionedRayTraceConfigurationBuilder raySize(final double raySize) {
Preconditions.checkArgument(raySize >= 0, "raySize must be non-negative");
this.raySize = raySize;
return this;
}
@Override
public PositionedRayTraceConfigurationBuilder entityFilter(final Predicate<? super Entity> entityFilter) {
Preconditions.checkArgument(entityFilter != null, "entityFilter must not be null");
this.entityFilter = entityFilter;
return this;
}
@Override
public PositionedRayTraceConfigurationBuilder blockFilter(final Predicate<? super Block> blockFilter) {
Preconditions.checkArgument(blockFilter != null, "blockFilter must not be null");
this.blockFilter = blockFilter;
return this;
}
@Override
public PositionedRayTraceConfigurationBuilder targets(final RayTraceTarget first, final RayTraceTarget... others) {
Preconditions.checkArgument(first != null, "first must not be null");
Preconditions.checkArgument(others != null, "others must not be null");
this.targets = EnumSet.of(first, others);
return this;
}
}

View File

@@ -97,21 +97,21 @@ public final class PaperRegistries {
start(Registries.MENU, RegistryKey.MENU).craft(MenuType.class, CraftMenuType::new).build(),
start(Registries.ATTRIBUTE, RegistryKey.ATTRIBUTE).craft(Attribute.class, CraftAttribute::new).build(),
start(Registries.FLUID, RegistryKey.FLUID).craft(Fluid.class, CraftFluid::new).build(),
start(Registries.SOUND_EVENT, RegistryKey.SOUND_EVENT).craft(Sound.class, CraftSound::new).build(),
start(Registries.SOUND_EVENT, RegistryKey.SOUND_EVENT).craft(Sound.class, CraftSound::new, true).build(),
start(Registries.DATA_COMPONENT_TYPE, RegistryKey.DATA_COMPONENT_TYPE).craft(DataComponentTypes.class, PaperDataComponentType::of).build(),
// data-drivens
start(Registries.BIOME, RegistryKey.BIOME).craft(Biome.class, CraftBiome::new).build().delayed(),
start(Registries.STRUCTURE, RegistryKey.STRUCTURE).craft(Structure.class, CraftStructure::new).build().delayed(),
start(Registries.TRIM_MATERIAL, RegistryKey.TRIM_MATERIAL).craft(TrimMaterial.class, CraftTrimMaterial::new).build().delayed(),
start(Registries.TRIM_PATTERN, RegistryKey.TRIM_PATTERN).craft(TrimPattern.class, CraftTrimPattern::new).build().delayed(),
start(Registries.TRIM_MATERIAL, RegistryKey.TRIM_MATERIAL).craft(TrimMaterial.class, CraftTrimMaterial::new, true).build().delayed(),
start(Registries.TRIM_PATTERN, RegistryKey.TRIM_PATTERN).craft(TrimPattern.class, CraftTrimPattern::new, true).build().delayed(),
start(Registries.DAMAGE_TYPE, RegistryKey.DAMAGE_TYPE).craft(DamageType.class, CraftDamageType::new).writable(PaperDamageTypeRegistryEntry.PaperBuilder::new).delayed(),
start(Registries.WOLF_VARIANT, RegistryKey.WOLF_VARIANT).craft(Wolf.Variant.class, CraftWolf.CraftVariant::new).build().delayed(),
start(Registries.ENCHANTMENT, RegistryKey.ENCHANTMENT).craft(Enchantment.class, CraftEnchantment::new).serializationUpdater(FieldRename.ENCHANTMENT_RENAME).writable(PaperEnchantmentRegistryEntry.PaperBuilder::new).delayed(),
start(Registries.JUKEBOX_SONG, RegistryKey.JUKEBOX_SONG).craft(JukeboxSong.class, CraftJukeboxSong::new).build().delayed(),
start(Registries.BANNER_PATTERN, RegistryKey.BANNER_PATTERN).craft(PatternType.class, CraftPatternType::new).writable(PaperBannerPatternRegistryEntry.PaperBuilder::new).delayed(),
start(Registries.PAINTING_VARIANT, RegistryKey.PAINTING_VARIANT).craft(Art.class, CraftArt::new).writable(PaperPaintingVariantRegistryEntry.PaperBuilder::new).delayed(),
start(Registries.INSTRUMENT, RegistryKey.INSTRUMENT).craft(MusicInstrument.class, CraftMusicInstrument::new).build().delayed(),
start(Registries.BANNER_PATTERN, RegistryKey.BANNER_PATTERN).craft(PatternType.class, CraftPatternType::new, true).writable(PaperBannerPatternRegistryEntry.PaperBuilder::new).delayed(),
start(Registries.PAINTING_VARIANT, RegistryKey.PAINTING_VARIANT).craft(Art.class, CraftArt::new, true).writable(PaperPaintingVariantRegistryEntry.PaperBuilder::new).delayed(),
start(Registries.INSTRUMENT, RegistryKey.INSTRUMENT).craft(MusicInstrument.class, CraftMusicInstrument::new, true).build().delayed(),
// api-only
start(Registries.ENTITY_TYPE, RegistryKey.ENTITY_TYPE).apiOnly(PaperSimpleRegistry::entityType),
@@ -153,12 +153,12 @@ public final class PaperRegistries {
@SuppressWarnings("unchecked")
public static <M, T> RegistryKey<T> registryFromNms(final ResourceKey<? extends Registry<M>> registryResourceKey) {
return (RegistryKey<T>) Objects.requireNonNull(BY_RESOURCE_KEY.get(registryResourceKey), registryResourceKey + " doesn't have an api RegistryKey").apiKey();
return (RegistryKey<T>) Objects.requireNonNull(BY_RESOURCE_KEY.get(registryResourceKey), () -> registryResourceKey + " doesn't have an api RegistryKey").apiKey();
}
@SuppressWarnings("unchecked")
public static <M, T> ResourceKey<? extends Registry<M>> registryToNms(final RegistryKey<T> registryKey) {
return (ResourceKey<? extends Registry<M>>) Objects.requireNonNull(BY_REGISTRY_KEY.get(registryKey), registryKey + " doesn't have an mc registry ResourceKey").mcKey();
return (ResourceKey<? extends Registry<M>>) Objects.requireNonNull(BY_REGISTRY_KEY.get(registryKey), () -> registryKey + " doesn't have an mc registry ResourceKey").mcKey();
}
public static <M, T> TypedKey<T> fromNms(final ResourceKey<M> resourceKey) {

View File

@@ -107,7 +107,7 @@ public class PaperRegistryAccess implements RegistryAccess {
public <M> void lockReferenceHolders(final ResourceKey<? extends net.minecraft.core.Registry<M>> resourceKey) {
final RegistryEntry<M, Keyed> entry = PaperRegistries.getEntry(resourceKey);
if (entry == null || !(entry.meta() instanceof final RegistryEntryMeta.ServerSide<M, Keyed> serverSide) || !serverSide.registryTypeMapper().supportsDirectHolders()) {
if (entry == null || !(entry.meta() instanceof final RegistryEntryMeta.ServerSide<M, Keyed> serverSide) || !serverSide.registryTypeMapper().constructorUsesHolder()) {
return;
}
final CraftRegistry<?, M> registry = (CraftRegistry<?, M>) this.getRegistry(entry.apiKey());

View File

@@ -24,7 +24,7 @@ public final class InlinedRegistryBuilderProviderImpl implements InlinedRegistry
Preconditions.checkArgument(buildableMeta.registryTypeMapper().supportsDirectHolders(), "Registry type mapper must support direct holders");
final PaperRegistryBuilderFactory<M, A, B> builderFactory = new PaperRegistryBuilderFactory<>(Conversions.global(), buildableMeta.builderFiller(), CraftRegistry.getMinecraftRegistry(buildableMeta.mcKey())::getValue);
value.accept(builderFactory);
return buildableMeta.registryTypeMapper().convertDirectHolder(Holder.direct(builderFactory.requireBuilder().build()));
return buildableMeta.registryTypeMapper().createBukkit(Holder.direct(builderFactory.requireBuilder().build()));
}
@Override

View File

@@ -1,11 +1,9 @@
package io.papermc.paper.registry.data;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import io.papermc.paper.registry.PaperRegistryBuilder;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.registry.TypedKey;
import io.papermc.paper.registry.data.util.Checks;
import io.papermc.paper.registry.data.util.Conversions;
import io.papermc.paper.registry.set.PaperRegistrySets;
@@ -189,7 +187,6 @@ public class PaperEnchantmentRegistryEntry implements EnchantmentRegistryEntry {
@Override
public Builder anvilCost(final @Range(from = 0, to = Integer.MAX_VALUE) int anvilCost) {
Preconditions.checkArgument(anvilCost >= 0, "anvilCost must be non-negative");
this.anvilCost = OptionalInt.of(asArgumentMin(anvilCost, "anvilCost", 0));
return this;
}

View File

@@ -42,7 +42,11 @@ public class RegistryEntryBuilder<M, A extends Keyed> { // TODO remove Keyed
}
public CraftStage<M, A> craft(final Class<?> classToPreload, final Function<Holder<M>, ? extends A> minecraftToBukkit) {
return new CraftStage<>(this.mcKey, this.apiKey, classToPreload, new RegistryTypeMapper<>(minecraftToBukkit));
return this.craft(classToPreload, minecraftToBukkit, false);
}
public CraftStage<M, A> craft(final Class<?> classToPreload, final Function<Holder<M>, ? extends A> minecraftToBukkit, final boolean allowDirect) {
return new CraftStage<>(this.mcKey, this.apiKey, classToPreload, new RegistryTypeMapper<>(minecraftToBukkit, allowDirect));
}
public static final class CraftStage<M, A extends Keyed> extends RegistryEntryBuilder<M, A> { // TODO remove Keyed

View File

@@ -60,7 +60,7 @@ public sealed interface RegistryEntryMeta<M, A extends Keyed> permits RegistryEn
) implements ServerSide<M, A> { // TODO remove Keyed
public Craft {
Preconditions.checkArgument(!classToPreload.getPackageName().startsWith("net.minecraft"), classToPreload + " should not be in the net.minecraft package as the class-to-preload");
Preconditions.checkArgument(!classToPreload.getPackageName().startsWith("net.minecraft"), "%s should not be in the net.minecraft package as the class-to-preload", classToPreload);
}
}

View File

@@ -1,7 +1,7 @@
package io.papermc.paper.registry.entry;
import com.google.common.base.Preconditions;
import com.mojang.datafixers.util.Either;
import io.papermc.paper.util.MCUtil;
import java.util.function.BiFunction;
import java.util.function.Function;
import net.minecraft.core.Holder;
@@ -9,29 +9,40 @@ import org.bukkit.NamespacedKey;
public final class RegistryTypeMapper<M, A> {
final Either<BiFunction<? super NamespacedKey, M, ? extends A>, Function<Holder<M>, ? extends A>> minecraftToBukkit;
private static <M, A> Function<Holder<M>, ? extends A> wrap(final BiFunction<? super NamespacedKey, M, ? extends A> byValueCreator) {
return holder -> {
if (!(holder instanceof final Holder.Reference<M> reference)) {
throw new IllegalArgumentException("This type does not support direct holders: " + holder);
}
return byValueCreator.apply(MCUtil.fromResourceKey(reference.key()), reference.value());
};
}
final Either<Function<Holder<M>, ? extends A>, BiFunction<? super NamespacedKey, M, ? extends A>> minecraftToBukkit;
private final boolean supportsDirectHolders;
public RegistryTypeMapper(final BiFunction<? super NamespacedKey, M, ? extends A> byValueCreator) {
this.minecraftToBukkit = Either.left(byValueCreator);
this.minecraftToBukkit = Either.right(byValueCreator);
this.supportsDirectHolders = false;
}
public RegistryTypeMapper(final Function<Holder<M>, ? extends A> byHolderCreator) {
this.minecraftToBukkit = Either.right(byHolderCreator);
public RegistryTypeMapper(final Function<Holder<M>, ? extends A> byHolderCreator, final boolean supportsDirectHolders) {
this.minecraftToBukkit = Either.left(byHolderCreator);
this.supportsDirectHolders = supportsDirectHolders;
}
public A createBukkit(final NamespacedKey key, final Holder<M> minecraft) {
return this.minecraftToBukkit.map(
minecraftToBukkit -> minecraftToBukkit.apply(key, minecraft.value()),
minecraftToBukkit -> minecraftToBukkit.apply(minecraft)
);
public A createBukkit(final Holder<M> minecraft) {
return this.minecraftToBukkit.<Function<Holder<M>, ? extends A>>map(
Function.identity(),
RegistryTypeMapper::wrap
).apply(minecraft);
}
public boolean supportsDirectHolders() {
return this.minecraftToBukkit.right().isPresent();
return this.supportsDirectHolders;
}
public A convertDirectHolder(final Holder<M> directHolder) {
Preconditions.checkArgument(this.supportsDirectHolders() && directHolder.kind() == Holder.Kind.DIRECT);
return this.minecraftToBukkit.right().orElseThrow().apply(directHolder);
public boolean constructorUsesHolder() {
return this.minecraftToBukkit.left().isPresent();
}
}

View File

@@ -34,7 +34,7 @@ public final class RegistryEventMap {
@SuppressWarnings("unchecked")
public <T, E extends LifecycleEvent> LifecycleEventType<BootstrapContext, E, ?> getEventType(final RegistryKey<T> registryKey) {
return (LifecycleEventType<BootstrapContext, E, ?>) Objects.requireNonNull(this.eventTypes.get(registryKey), "No hook for " + registryKey);
return (LifecycleEventType<BootstrapContext, E, ?>) Objects.requireNonNull(this.eventTypes.get(registryKey), () -> "No hook for " + registryKey);
}
public boolean hasHandlers(final RegistryKey<?> registryKey) {

View File

@@ -49,6 +49,11 @@ public final class DelayedRegistry<T extends Keyed, R extends Registry<T>> imple
return this.delegate().stream();
}
@Override
public int size() {
return this.delegate().size();
}
@Override
public @Nullable NamespacedKey getKey(final T value) {
return this.delegate().getKey(value);

View File

@@ -181,7 +181,7 @@ public final class FoliaAsyncScheduler implements AsyncScheduler {
private void setDelay(final ScheduledFuture<?> delay) {
this.delay = delay;
this.state = STATE_SCHEDULED_EXECUTOR;
this.state = delay == null ? STATE_SCHEDULED_EXECUTOR : STATE_ON_TIMER;
}
@Override

View File

@@ -4,6 +4,8 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import net.kyori.adventure.key.Key;
import net.minecraft.core.Holder;
import net.minecraft.resources.RegistryOps;
@@ -25,7 +27,8 @@ public interface Holderable<M> extends Handleable<M> {
return this.getHolder().value();
}
static <T extends org.bukkit.Keyed, M> @Nullable T fromBukkitSerializationObject(final Object deserialized, final Codec<? extends Holder<M>> codec, final Registry<T> registry) { // TODO remove Keyed
static <T extends org.bukkit.Keyed, M> @Nullable T fromBukkitSerializationObject(final Object deserialized, final Codec<M> directCodec, final RegistryKey<T> registryKey) { // TODO remove Keyed
final Registry<T> registry = RegistryAccess.registryAccess().getRegistry(registryKey);
return switch (deserialized) {
case @Subst("key:value") final String string -> {
if (!(Key.parseable(string))) {
@@ -38,18 +41,18 @@ public interface Holderable<M> extends Handleable<M> {
throw new IllegalArgumentException("Cannot deserialize direct holders for " + registry);
}
final RegistryOps<JsonElement> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JsonOps.INSTANCE);
final Holder<M> holder = codec.decode(ops, element).getOrThrow().getFirst();
yield ((CraftRegistry<T, M>) registry).convertDirectHolder(holder);
final M holder = directCodec.decode(ops, element).getOrThrow().getFirst();
yield ((CraftRegistry<T, M>) registry).createBukkit(Holder.direct(holder));
}
default -> throw new IllegalArgumentException("Cannot deserialize " + deserialized);
};
}
default Object toBukkitSerializationObject(final Codec<? super Holder<M>> codec) {
default Object toBukkitSerializationObject(final Codec<? super M> directCodec) {
return switch (this.getHolder()) {
case final Holder.Direct<M> direct -> {
final RegistryOps<JsonElement> ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(JsonOps.INSTANCE);
yield new JsonObjectWrapper(codec.encodeStart(ops, direct).getOrThrow().getAsJsonObject());
yield new JsonObjectWrapper(directCodec.encodeStart(ops, direct.value()).getOrThrow().getAsJsonObject());
}
case final Holder.Reference<M> reference -> reference.key().location().toString();
default -> throw new IllegalArgumentException("Cannot serialize " + this.getHolder());

View File

@@ -0,0 +1,101 @@
package io.papermc.paper.util;
import com.google.common.collect.ImmutableMap;
import io.papermc.paper.configuration.GlobalConfiguration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.UnaryOperator;
import net.minecraft.Util;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.Registries;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.alchemy.PotionContents;
import net.minecraft.world.item.component.LodestoneTracker;
import net.minecraft.world.item.enchantment.ItemEnchantments;
import org.jspecify.annotations.NullMarked;
@NullMarked
public final class ItemComponentSanitizer {
/*
* This returns for types, that when configured to be serialized, should instead return these objects.
* This is possibly because dropping the patched type may introduce visual changes.
*/
static final Map<DataComponentType<?>, UnaryOperator<?>> SANITIZATION_OVERRIDES = Util.make(ImmutableMap.<DataComponentType<?>, UnaryOperator<?>>builder(), (map) -> {
put(map, DataComponents.LODESTONE_TRACKER, empty(new LodestoneTracker(Optional.empty(), false))); // We need it to be present to keep the glint
put(map, DataComponents.POTION_CONTENTS, ItemComponentSanitizer::sanitizePotionContents); // Custom situational serialization
if (MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.ENCHANTMENT).size() > 0) {
put(map, DataComponents.ENCHANTMENTS, empty(dummyEnchantments())); // We need to keep it present to keep the glint
put(map, DataComponents.STORED_ENCHANTMENTS, empty(dummyEnchantments())); // We need to keep it present to keep the glint
}
}
).build();
@SuppressWarnings({"rawtypes", "unchecked"})
private static <T> void put(final ImmutableMap.Builder map, final DataComponentType<T> type, final UnaryOperator<T> object) {
map.put(type, object);
}
private static <T> UnaryOperator<T> empty(final T object) {
return (unused) -> object;
}
private static PotionContents sanitizePotionContents(final PotionContents potionContents) {
// We have a custom color! We can hide everything!
if (potionContents.customColor().isPresent()) {
return new PotionContents(Optional.empty(), potionContents.customColor(), List.of(), Optional.empty());
}
// WE cannot hide anything really, as the color is a mix of potion/potion contents, which can
// possibly be reversed.
return potionContents;
}
// We cant use the empty map from enchantments because we want to keep the glow
private static ItemEnchantments dummyEnchantments() {
final ItemEnchantments.Mutable obj = new ItemEnchantments.Mutable(ItemEnchantments.EMPTY);
obj.set(MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getRandom(RandomSource.create()).orElseThrow(), 1);
return obj.toImmutable();
}
public static int sanitizeCount(final ItemObfuscationSession obfuscationSession, final ItemStack itemStack, final int count) {
if (obfuscationSession.obfuscationLevel() != ItemObfuscationSession.ObfuscationLevel.ALL) return count; // Ignore if we are not obfuscating
if (GlobalConfiguration.get().anticheat.obfuscation.items.binding.getAssetObfuscation(itemStack).sanitizeCount()) {
return 1;
} else {
return count;
}
}
public static boolean shouldDrop(final ItemObfuscationSession obfuscationSession, final DataComponentType<?> key) {
if (obfuscationSession.obfuscationLevel() != ItemObfuscationSession.ObfuscationLevel.ALL) return false; // Ignore if we are not obfuscating
final ItemStack targetItemstack = obfuscationSession.context().itemStack();
// Only drop if configured to do so.
return GlobalConfiguration.get().anticheat.obfuscation.items.binding.getAssetObfuscation(targetItemstack).patchStrategy().get(key) == ItemObfuscationBinding.BoundObfuscationConfiguration.MutationType.Drop.INSTANCE;
}
public static Optional<?> override(final ItemObfuscationSession obfuscationSession, final DataComponentType<?> key, final Optional<?> value) {
if (obfuscationSession.obfuscationLevel() != ItemObfuscationSession.ObfuscationLevel.ALL) return value; // Ignore if we are not obfuscating
// Ignore removed values
if (value.isEmpty()) {
return value;
}
final ItemStack targetItemstack = obfuscationSession.context().itemStack();
return switch (GlobalConfiguration.get().anticheat.obfuscation.items.binding.getAssetObfuscation(targetItemstack).patchStrategy().get(key)) {
case final ItemObfuscationBinding.BoundObfuscationConfiguration.MutationType.Drop ignored -> Optional.empty();
case final ItemObfuscationBinding.BoundObfuscationConfiguration.MutationType.Sanitize sanitize -> Optional.of(sanitize.sanitizer().apply(value.get()));
case null -> value;
};
}
}

View File

@@ -0,0 +1,133 @@
package io.papermc.paper.util;
import io.papermc.paper.configuration.GlobalConfiguration;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.component.DataComponents;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.NullMarked;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Required;
/**
* The item obfuscation binding is a state bound by the configured item obfuscation.
* It only hosts the bound and computed data from the global configuration.
*/
@NullMarked
public final class ItemObfuscationBinding {
public final ItemObfuscationSession.ObfuscationLevel level;
private final BoundObfuscationConfiguration base;
private final Map<ResourceLocation, BoundObfuscationConfiguration> overrides;
public ItemObfuscationBinding(final GlobalConfiguration.Anticheat.Obfuscation.Items items) {
this.level = items.enableItemObfuscation ? ItemObfuscationSession.ObfuscationLevel.ALL : ItemObfuscationSession.ObfuscationLevel.OVERSIZED;
this.base = bind(items.allModels);
final Map<ResourceLocation, BoundObfuscationConfiguration> overrides = new HashMap<>();
for (final Map.Entry<ResourceLocation, AssetObfuscationConfiguration> entry : items.modelOverrides.entrySet()) {
overrides.put(entry.getKey(), bind(entry.getValue()));
}
this.overrides = Collections.unmodifiableMap(overrides);
}
public record BoundObfuscationConfiguration(boolean sanitizeCount,
Map<DataComponentType<?>, MutationType> patchStrategy) {
sealed interface MutationType permits MutationType.Drop, MutationType.Sanitize {
enum Drop implements MutationType {
INSTANCE
}
record Sanitize(UnaryOperator sanitizer) implements MutationType {
}
}
}
@ConfigSerializable
public record AssetObfuscationConfiguration(@Required boolean sanitizeCount,
Set<DataComponentType<?>> dontObfuscate,
Set<DataComponentType<?>> alsoObfuscate) {
}
private static BoundObfuscationConfiguration bind(final AssetObfuscationConfiguration config) {
final Set<DataComponentType<?>> base = new HashSet<>(BASE_OVERRIDERS);
base.addAll(config.alsoObfuscate());
base.removeAll(config.dontObfuscate());
final Map<DataComponentType<?>, BoundObfuscationConfiguration.MutationType> finalStrategy = new HashMap<>();
// Configure what path the data component should go through, should it be dropped, or should it be sanitized?
for (final DataComponentType<?> type : base) {
// We require some special logic, sanitize it rather than dropping it.
final UnaryOperator<?> sanitizationOverride = ItemComponentSanitizer.SANITIZATION_OVERRIDES.get(type);
if (sanitizationOverride != null) {
finalStrategy.put(type, new BoundObfuscationConfiguration.MutationType.Sanitize(sanitizationOverride));
} else {
finalStrategy.put(type, BoundObfuscationConfiguration.MutationType.Drop.INSTANCE);
}
}
return new BoundObfuscationConfiguration(config.sanitizeCount(), finalStrategy);
}
public BoundObfuscationConfiguration getAssetObfuscation(final ItemStack itemStack) {
if (this.overrides.isEmpty()) {
return this.base;
}
return this.overrides.getOrDefault(itemStack.get(DataComponents.ITEM_MODEL), this.base);
}
static final Set<DataComponentType<?>> BASE_OVERRIDERS = Set.of(
DataComponents.MAX_STACK_SIZE,
DataComponents.MAX_DAMAGE,
DataComponents.DAMAGE,
DataComponents.UNBREAKABLE,
DataComponents.CUSTOM_NAME,
DataComponents.ITEM_NAME,
DataComponents.LORE,
DataComponents.RARITY,
DataComponents.ENCHANTMENTS,
DataComponents.CAN_PLACE_ON,
DataComponents.CAN_BREAK,
DataComponents.ATTRIBUTE_MODIFIERS,
DataComponents.HIDE_ADDITIONAL_TOOLTIP,
DataComponents.HIDE_TOOLTIP,
DataComponents.REPAIR_COST,
DataComponents.USE_REMAINDER,
DataComponents.FOOD,
DataComponents.DAMAGE_RESISTANT,
// Not important on the player
DataComponents.TOOL,
DataComponents.ENCHANTABLE,
DataComponents.REPAIRABLE,
DataComponents.GLIDER,
DataComponents.TOOLTIP_STYLE,
DataComponents.DEATH_PROTECTION,
DataComponents.STORED_ENCHANTMENTS,
DataComponents.MAP_ID,
DataComponents.POTION_CONTENTS,
DataComponents.SUSPICIOUS_STEW_EFFECTS,
DataComponents.WRITABLE_BOOK_CONTENT,
DataComponents.WRITTEN_BOOK_CONTENT,
DataComponents.CUSTOM_DATA,
DataComponents.ENTITY_DATA,
DataComponents.BUCKET_ENTITY_DATA,
DataComponents.BLOCK_ENTITY_DATA,
DataComponents.INSTRUMENT,
DataComponents.OMINOUS_BOTTLE_AMPLIFIER,
DataComponents.JUKEBOX_PLAYABLE,
DataComponents.LODESTONE_TRACKER,
DataComponents.FIREWORKS,
DataComponents.NOTE_BLOCK_SOUND,
DataComponents.BEES,
DataComponents.LOCK,
DataComponents.CONTAINER_LOOT
);
}

View File

@@ -0,0 +1,114 @@
package io.papermc.paper.util;
import java.util.function.UnaryOperator;
import com.google.common.base.Preconditions;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
/**
* The item obfuscation session may be started by a thread to indicate that items should be obfuscated when serialized
* for network usage.
* <p>
* A session is persistent throughout an entire thread and will be "activated" by passing an {@link ObfuscationContext}
* to start/switch context methods.
*/
@NullMarked
public class ItemObfuscationSession implements SafeAutoClosable {
static final ThreadLocal<ItemObfuscationSession> THREAD_LOCAL_SESSION = ThreadLocal.withInitial(ItemObfuscationSession::new);
public static ItemObfuscationSession currentSession() {
return THREAD_LOCAL_SESSION.get();
}
/**
* Obfuscation level on a specific context.
*/
public enum ObfuscationLevel {
NONE,
OVERSIZED,
ALL;
public boolean obfuscateOversized() {
return switch (this) {
case OVERSIZED, ALL -> true;
default -> false;
};
}
public boolean isObfuscating() {
return this != NONE;
}
}
public static ItemObfuscationSession start(final ObfuscationLevel level) {
final ItemObfuscationSession sanitizer = THREAD_LOCAL_SESSION.get();
sanitizer.switchContext(new ObfuscationContext(sanitizer, null, null, level));
return sanitizer;
}
/**
* Updates the context of the currently running session by requiring the unary operator to emit a new context
* based on the current one.
* The method expects the caller to use the withers on the context.
*
* @param contextUpdater the operator to construct the new context.
* @return the context callback to close once the context expires.
*/
public static SafeAutoClosable withContext(final UnaryOperator<ObfuscationContext> contextUpdater) {
final ItemObfuscationSession session = THREAD_LOCAL_SESSION.get();
// Don't pass any context if we are not currently sanitizing
if (!session.obfuscationLevel().isObfuscating()) return () -> {
};
final ObfuscationContext newContext = contextUpdater.apply(session.context());
Preconditions.checkState(newContext != session.context(), "withContext yielded same context instance, this will break the stack on close");
session.switchContext(newContext);
return newContext;
}
private final ObfuscationContext root = new ObfuscationContext(this, null, null, ObfuscationLevel.NONE);
private ObfuscationContext context = root;
public void switchContext(final ObfuscationContext context) {
this.context = context;
}
public ObfuscationContext context() {
return this.context;
}
@Override
public void close() {
this.context = root;
}
public ObfuscationLevel obfuscationLevel() {
return this.context.level;
}
public record ObfuscationContext(
ItemObfuscationSession parent,
@Nullable ObfuscationContext previousContext,
@Nullable ItemStack itemStack,
ObfuscationLevel level
) implements SafeAutoClosable {
public ObfuscationContext itemStack(final ItemStack itemStack) {
return new ObfuscationContext(this.parent, this, itemStack, this.level);
}
public ObfuscationContext level(final ObfuscationLevel obfuscationLevel) {
return new ObfuscationContext(this.parent, this, this.itemStack, obfuscationLevel);
}
@Override
public void close() {
// Restore the previous context when this context is closed.
this.parent().switchContext(this.previousContext);
}
}
}

View File

@@ -1,9 +1,8 @@
package io.papermc.paper.util;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.UnaryOperator;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.List;
import java.util.function.UnaryOperator;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.util.Mth;
@@ -12,26 +11,38 @@ import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.BundleContents;
import net.minecraft.world.item.component.ChargedProjectiles;
import net.minecraft.world.item.component.ItemContainerContents;
import org.apache.commons.lang3.math.Fraction;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;
@DefaultQualifier(NonNull.class)
public final class DataSanitizationUtil {
public final class OversizedItemComponentSanitizer {
private static final ThreadLocal<DataSanitizer> DATA_SANITIZER = ThreadLocal.withInitial(DataSanitizer::new);
public static DataSanitizer start(final boolean sanitize) {
final DataSanitizer sanitizer = DATA_SANITIZER.get();
if (sanitize) {
sanitizer.start();
}
return sanitizer;
}
public static final StreamCodec<RegistryFriendlyByteBuf, ChargedProjectiles> CHARGED_PROJECTILES = codec(ChargedProjectiles.STREAM_CODEC, DataSanitizationUtil::sanitizeChargedProjectiles);
public static final StreamCodec<RegistryFriendlyByteBuf, BundleContents> BUNDLE_CONTENTS = codec(BundleContents.STREAM_CODEC, DataSanitizationUtil::sanitizeBundleContents);
/*
These represent codecs that are meant to help get rid of possibly big items by ALWAYS hiding this data.
*/
public static final StreamCodec<RegistryFriendlyByteBuf, ChargedProjectiles> CHARGED_PROJECTILES = codec(ChargedProjectiles.STREAM_CODEC, OversizedItemComponentSanitizer::sanitizeChargedProjectiles);
public static final StreamCodec<RegistryFriendlyByteBuf, ItemContainerContents> CONTAINER = codec(ItemContainerContents.STREAM_CODEC, contents -> ItemContainerContents.EMPTY);
public static final StreamCodec<RegistryFriendlyByteBuf, BundleContents> BUNDLE_CONTENTS = new StreamCodec<>() {
@Override
public BundleContents decode(final RegistryFriendlyByteBuf buffer) {
return BundleContents.STREAM_CODEC.decode(buffer);
}
@Override
public void encode(final RegistryFriendlyByteBuf buffer, final BundleContents value) {
if (!ItemObfuscationSession.currentSession().obfuscationLevel().obfuscateOversized()) {
BundleContents.STREAM_CODEC.encode(buffer, value);
return;
}
// Disable further obfuscation to skip e.g. count.
try (final SafeAutoClosable ignored = ItemObfuscationSession.withContext(c -> c.level(ItemObfuscationSession.ObfuscationLevel.OVERSIZED))){
BundleContents.STREAM_CODEC.encode(buffer, sanitizeBundleContents(value));
}
}
};
private static <B, A> StreamCodec<B, A> codec(final StreamCodec<B, A> delegate, final UnaryOperator<A> sanitizer) {
return new DataSanitizationCodec<>(delegate, sanitizer);
}
private static ChargedProjectiles sanitizeChargedProjectiles(final ChargedProjectiles projectiles) {
if (projectiles.isEmpty()) {
@@ -39,21 +50,26 @@ public final class DataSanitizationUtil {
}
return ChargedProjectiles.of(List.of(
new ItemStack(projectiles.contains(Items.FIREWORK_ROCKET) ? Items.FIREWORK_ROCKET : Items.ARROW)
));
new ItemStack(
projectiles.contains(Items.FIREWORK_ROCKET)
? Items.FIREWORK_ROCKET
: Items.ARROW
)));
}
// Although bundles no longer change their size based on fullness, fullness is exposed in item models.
private static BundleContents sanitizeBundleContents(final BundleContents contents) {
if (contents.isEmpty()) {
return contents;
}
// Bundles change their texture based on their fullness.
// A bundles content weight may be anywhere from 0 to, basically, infinity.
// A weight of 1 is the usual maximum case
int sizeUsed = Mth.mulAndTruncate(contents.weight(), 64);
// Early out, *most* bundles should not be overfilled above a weight of one.
if (sizeUsed <= 64) return new BundleContents(List.of(new ItemStack(Items.PAPER, Math.max(1, sizeUsed))));
if (sizeUsed <= 64) {
return new BundleContents(List.of(new ItemStack(Items.PAPER, Math.max(1, sizeUsed))));
}
final List<ItemStack> sanitizedRepresentation = new ObjectArrayList<>(sizeUsed / 64 + 1);
while (sizeUsed > 0) {
@@ -66,20 +82,19 @@ public final class DataSanitizationUtil {
return new BundleContents(sanitizedRepresentation);
}
private static <B, A> StreamCodec<B, A> codec(final StreamCodec<B, A> delegate, final UnaryOperator<A> sanitizer) {
return new DataSanitizationCodec<>(delegate, sanitizer);
}
private record DataSanitizationCodec<B, A>(StreamCodec<B, A> delegate, UnaryOperator<A> sanitizer) implements StreamCodec<B, A> {
// Codec used to override encoding if sanitization is enabled
private record DataSanitizationCodec<B, A>(StreamCodec<B, A> delegate,
UnaryOperator<A> sanitizer) implements StreamCodec<B, A> {
@Override
public @NonNull A decode(final @NonNull B buf) {
return this.delegate.decode(buf);
}
@SuppressWarnings("resource")
@Override
public void encode(final @NonNull B buf, final @NonNull A value) {
if (!DATA_SANITIZER.get().value().get()) {
if (!ItemObfuscationSession.currentSession().obfuscationLevel().obfuscateOversized()) {
this.delegate.encode(buf, value);
} else {
this.delegate.encode(buf, this.sanitizer.apply(value));
@@ -87,22 +102,4 @@ public final class DataSanitizationUtil {
}
}
public record DataSanitizer(AtomicBoolean value) implements AutoCloseable {
public DataSanitizer() {
this(new AtomicBoolean(false));
}
public void start() {
this.value.compareAndSet(false, true);
}
@Override
public void close() {
this.value.compareAndSet(true, false);
}
}
private DataSanitizationUtil() {
}
}

View File

@@ -0,0 +1,10 @@
package io.papermc.paper.util;
/**
* A type of {@link AutoCloseable} that does not throw a checked exception.
*/
public interface SafeAutoClosable extends AutoCloseable {
@Override
void close();
}