diff --git a/paper-server/patches/sources/net/minecraft/core/component/DataComponentPatch.java.patch b/paper-server/patches/sources/net/minecraft/core/component/DataComponentPatch.java.patch index 9a4fca81b5b3..9850a5e8f911 100644 --- a/paper-server/patches/sources/net/minecraft/core/component/DataComponentPatch.java.patch +++ b/paper-server/patches/sources/net/minecraft/core/component/DataComponentPatch.java.patch @@ -1,5 +1,34 @@ --- a/net/minecraft/core/component/DataComponentPatch.java +++ b/net/minecraft/core/component/DataComponentPatch.java +@@ -86,6 +_,11 @@ + buffer.writeVarInt(0); + buffer.writeVarInt(0); + } else { ++ // Paper start - data sanitization for items ++ final io.papermc.paper.util.ItemObfuscationSession itemObfuscationSession = value.map.isEmpty() ++ ? null // Avoid thread local lookup of current session if it won't be needed anyway. ++ : io.papermc.paper.util.ItemObfuscationSession.currentSession(); ++ // Paper end - data sanitization for items + int i = 0; + int i1 = 0; + +@@ -93,7 +_,7 @@ + value.map + )) { + if (entry.getValue().isPresent()) { +- i++; ++ if (!io.papermc.paper.util.ItemComponentSanitizer.shouldDrop(itemObfuscationSession, entry.getKey())) i++; // Paper - data sanitization for items + } else { + i1++; + } +@@ -106,6 +_,7 @@ + value.map + )) { + Optional optional = entryx.getValue(); ++ optional = io.papermc.paper.util.ItemComponentSanitizer.override(itemObfuscationSession, entryx.getKey(), entryx.getValue()); // Paper - data sanitization for items + if (optional.isPresent()) { + DataComponentType dataComponentType = entryx.getKey(); + DataComponentType.STREAM_CODEC.encode(buffer, dataComponentType); @@ -125,7 +_,13 @@ } diff --git a/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch b/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch index c1f061a0af9a..a1a9d2377849 100644 --- a/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch +++ b/paper-server/patches/sources/net/minecraft/core/component/DataComponents.java.patch @@ -5,11 +5,11 @@ ); public static final DataComponentType CHARGED_PROJECTILES = register( - "charged_projectiles", builder -> builder.persistent(ChargedProjectiles.CODEC).networkSynchronized(ChargedProjectiles.STREAM_CODEC).cacheEncoding() -+ "charged_projectiles", builder -> builder.persistent(ChargedProjectiles.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.CHARGED_PROJECTILES).cacheEncoding() // Paper - sanitize charged projectiles ++ "charged_projectiles", builder -> builder.persistent(ChargedProjectiles.CODEC).networkSynchronized(io.papermc.paper.util.OversizedItemComponentSanitizer.CHARGED_PROJECTILES).cacheEncoding() // Paper - sanitize charged projectiles ); public static final DataComponentType BUNDLE_CONTENTS = register( - "bundle_contents", builder -> builder.persistent(BundleContents.CODEC).networkSynchronized(BundleContents.STREAM_CODEC).cacheEncoding() -+ "bundle_contents", builder -> builder.persistent(BundleContents.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.BUNDLE_CONTENTS).cacheEncoding() // Paper - sanitize bundle contents ++ "bundle_contents", builder -> builder.persistent(BundleContents.CODEC).networkSynchronized(io.papermc.paper.util.OversizedItemComponentSanitizer.BUNDLE_CONTENTS).cacheEncoding() // Paper - sanitize bundle contents ); public static final DataComponentType POTION_CONTENTS = register( "potion_contents", builder -> builder.persistent(PotionContents.CODEC).networkSynchronized(PotionContents.STREAM_CODEC).cacheEncoding() @@ -18,7 +18,7 @@ ); public static final DataComponentType CONTAINER = register( - "container", builder -> builder.persistent(ItemContainerContents.CODEC).networkSynchronized(ItemContainerContents.STREAM_CODEC).cacheEncoding() -+ "container", builder -> builder.persistent(ItemContainerContents.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.CONTAINER).cacheEncoding() // Paper - sanitize container contents ++ "container", builder -> builder.persistent(ItemContainerContents.CODEC).networkSynchronized(io.papermc.paper.util.OversizedItemComponentSanitizer.CONTAINER).cacheEncoding() // Paper - sanitize container contents ); public static final DataComponentType BLOCK_STATE = register( "block_state", builder -> builder.persistent(BlockItemStateProperties.CODEC).networkSynchronized(BlockItemStateProperties.STREAM_CODEC).cacheEncoding() diff --git a/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java.patch index cb3ea7540d86..d98c5525116d 100644 --- a/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java.patch +++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java.patch @@ -4,7 +4,7 @@ } private static void pack(List> dataValues, RegistryFriendlyByteBuf buffer) { -+ try (io.papermc.paper.util.DataSanitizationUtil.DataSanitizer ignored = io.papermc.paper.util.DataSanitizationUtil.start(true)) { // Paper - data sanitization ++ try (io.papermc.paper.util.ItemObfuscationSession ignored = io.papermc.paper.util.ItemObfuscationSession.start(io.papermc.paper.configuration.GlobalConfiguration.get().anticheat.obfuscation.items.binding.level)) { // Paper - data sanitization for (SynchedEntityData.DataValue dataValue : dataValues) { dataValue.write(buffer); } diff --git a/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java.patch b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java.patch index d5959b28667e..0021ba7c530b 100644 --- a/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java.patch +++ b/paper-server/patches/sources/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java.patch @@ -18,7 +18,7 @@ buffer.writeVarInt(this.entity); int size = this.slots.size(); -+ try (io.papermc.paper.util.DataSanitizationUtil.DataSanitizer ignored = io.papermc.paper.util.DataSanitizationUtil.start(this.sanitize)) { // Paper - data sanitization ++ try (final io.papermc.paper.util.ItemObfuscationSession ignored = io.papermc.paper.util.ItemObfuscationSession.start(this.sanitize ? io.papermc.paper.configuration.GlobalConfiguration.get().anticheat.obfuscation.items.binding.level : io.papermc.paper.util.ItemObfuscationSession.ObfuscationLevel.NONE)) { // Paper - data sanitization for (int i = 0; i < size; i++) { Pair pair = this.slots.get(i); EquipmentSlot equipmentSlot = pair.getFirst(); diff --git a/paper-server/patches/sources/net/minecraft/world/item/ItemStack.java.patch b/paper-server/patches/sources/net/minecraft/world/item/ItemStack.java.patch index 141de8193afd..297a82f02d1d 100644 --- a/paper-server/patches/sources/net/minecraft/world/item/ItemStack.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/item/ItemStack.java.patch @@ -21,14 +21,15 @@ + if (value.isEmpty() || value.getItem() == null) { // CraftBukkit - NPE fix itemstack.getItem() buffer.writeVarInt(0); } else { - buffer.writeVarInt(value.getCount()); +- buffer.writeVarInt(value.getCount()); ++ buffer.writeVarInt(io.papermc.paper.util.ItemComponentSanitizer.sanitizeCount(io.papermc.paper.util.ItemObfuscationSession.currentSession(), value, value.getCount())); // Paper - potentially sanitize count ITEM_STREAM_CODEC.encode(buffer, value.getItemHolder()); + // Spigot start - filter + // value = value.copy(); + // CraftItemStack.setItemMeta(value, CraftItemStack.getItemMeta(value)); // Paper - This is no longer with raw NBT being handled in metadata + // Paper start - adventure; conditionally render translatable components + boolean prev = net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.get(); -+ try { ++ try (final io.papermc.paper.util.SafeAutoClosable ignored = io.papermc.paper.util.ItemObfuscationSession.withContext(c -> c.itemStack(value))) { // pass the itemstack as context to the obfuscation session + net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(true); DataComponentPatch.STREAM_CODEC.encode(buffer, value.components.asPatch()); + } finally { diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java b/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java index 109e569b7ba6..4a9258b62db3 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java @@ -80,7 +80,7 @@ protected ObjectMapper.Factory.Builder createGlobalObjectMapperFactoryBuilder() } @MustBeInvokedByOverriders - protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder() { + protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder(RegistryAccess registryAccess) { return this.createLoaderBuilder(); } @@ -104,7 +104,7 @@ static CheckedFunction reloade } 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 @@ private void trySaveFileNode(YamlConfigurationLoader loader, ConfigurationNode n } } - protected G initializeGlobalConfiguration(final CheckedFunction creator) throws ConfigurateException { + protected G initializeGlobalConfiguration(final RegistryAccess registryAccess, final CheckedFunction 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(); diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java index 088b8fe5d144..0b8e0ff5ce62 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java @@ -5,10 +5,14 @@ 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 net.minecraft.world.item.Items; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.spongepowered.configurate.objectmapping.ConfigSerializable; @@ -20,6 +24,7 @@ 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 +74,7 @@ public class ChunkLoadingAdvanced extends ConfigurationPart { ) public int playerMaxConcurrentChunkGenerates = 0; } - static void set(GlobalConfiguration instance) { + static void set(final GlobalConfiguration instance) { GlobalConfiguration.instance = instance; } @@ -354,4 +359,41 @@ public class BlockUpdates 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 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); + } + } + } + } } diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java index 098ab351de4f..e48fa405d92f 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java @@ -10,6 +10,7 @@ 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.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 @@ private static ConfigurationOptions defaultOptions(ConfigurationOptions options) .register(Duration.SERIALIZER) .register(DurationOrDisabled.SERIALIZER) .register(NbtPathSerializer.SERIALIZER) + .register(ResourceLocationSerializer.INSTANCE) ); } @@ -193,16 +196,17 @@ private static ObjectMapper.Factory.Builder defaultGlobalFactoryBuilder(ObjectMa } @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>() {}, registryAccess, Registries.DATA_COMPONENT_TYPE, false)) ); } @@ -316,7 +320,7 @@ protected boolean isConfigType(final Type type) { 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 static YamlConfiguration loadLegacyConfigFile(File configFile) throws Exc } @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); } diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java index f5ac1b029066..d7c9acaffdcf 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java @@ -88,17 +88,6 @@ public boolean isDefault() { 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 { diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/serializer/ResourceLocationSerializer.java b/paper-server/src/main/java/io/papermc/paper/configuration/serializer/ResourceLocationSerializer.java new file mode 100644 index 000000000000..4bb82632260d --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/configuration/serializer/ResourceLocationSerializer.java @@ -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 { + + public static final ScalarSerializer 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> typeSupported) { + return item.toString(); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/ItemComponentSanitizer.java b/paper-server/src/main/java/io/papermc/paper/util/ItemComponentSanitizer.java new file mode 100644 index 000000000000..15236ed57721 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/ItemComponentSanitizer.java @@ -0,0 +1,98 @@ +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, UnaryOperator> SANITIZATION_OVERRIDES = Util.make(ImmutableMap., 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.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 + put(map, DataComponents.POTION_CONTENTS, ItemComponentSanitizer::sanitizePotionContents); // Custom situational serialization + } + ).build(); + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void put(final ImmutableMap.Builder map, final DataComponentType type, final UnaryOperator object) { + map.put(type, object); + } + + private static UnaryOperator 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; + }; + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/ItemObfuscationBinding.java b/paper-server/src/main/java/io/papermc/paper/util/ItemObfuscationBinding.java new file mode 100644 index 000000000000..db7ac8a93b6a --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/ItemObfuscationBinding.java @@ -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 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 overrides = new HashMap<>(); + for (final Map.Entry entry : items.modelOverrides.entrySet()) { + overrides.put(entry.getKey(), bind(entry.getValue())); + } + this.overrides = Collections.unmodifiableMap(overrides); + } + + public record BoundObfuscationConfiguration(boolean sanitizeCount, + Map, 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> dontObfuscate, + Set> alsoObfuscate) { + + } + + private static BoundObfuscationConfiguration bind(final AssetObfuscationConfiguration config) { + final Set> base = new HashSet<>(BASE_OVERRIDERS); + base.addAll(config.alsoObfuscate()); + base.removeAll(config.dontObfuscate()); + + final Map, 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> 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 + ); +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/ItemObfuscationSession.java b/paper-server/src/main/java/io/papermc/paper/util/ItemObfuscationSession.java new file mode 100644 index 000000000000..deafa923d562 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/ItemObfuscationSession.java @@ -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. + *

+ * 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 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 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); + } + } + +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java b/paper-server/src/main/java/io/papermc/paper/util/OversizedItemComponentSanitizer.java similarity index 61% rename from paper-server/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java rename to paper-server/src/main/java/io/papermc/paper/util/OversizedItemComponentSanitizer.java index 72483dedd3b1..56f3a9324d16 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java +++ b/paper-server/src/main/java/io/papermc/paper/util/OversizedItemComponentSanitizer.java @@ -1,9 +1,8 @@ package io.papermc.paper.util; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.UnaryOperator; -import it.unimi.dsi.fastutil.objects.ObjectArrayList; 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.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 { + + /* + These represent codecs that are meant to help get rid of possibly big items by ALWAYS hiding this data. + */ + public static final StreamCodec CHARGED_PROJECTILES = codec(ChargedProjectiles.STREAM_CODEC, OversizedItemComponentSanitizer::sanitizeChargedProjectiles); + public static final StreamCodec CONTAINER = codec(ItemContainerContents.STREAM_CODEC, contents -> ItemContainerContents.EMPTY); + public static final StreamCodec BUNDLE_CONTENTS = new StreamCodec<>() { + @Override + public BundleContents decode(final RegistryFriendlyByteBuf buffer) { + return BundleContents.STREAM_CODEC.decode(buffer); + } - private static final ThreadLocal DATA_SANITIZER = ThreadLocal.withInitial(DataSanitizer::new); + @Override + public void encode(final RegistryFriendlyByteBuf buffer, final BundleContents value) { + if (!ItemObfuscationSession.currentSession().obfuscationLevel().obfuscateOversized()) { + BundleContents.STREAM_CODEC.encode(buffer, value); + return; + } - public static DataSanitizer start(final boolean sanitize) { - final DataSanitizer sanitizer = DATA_SANITIZER.get(); - if (sanitize) { - sanitizer.start(); + // 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)); + } } - return sanitizer; - } + }; - public static final StreamCodec CHARGED_PROJECTILES = codec(ChargedProjectiles.STREAM_CODEC, DataSanitizationUtil::sanitizeChargedProjectiles); - public static final StreamCodec BUNDLE_CONTENTS = codec(BundleContents.STREAM_CODEC, DataSanitizationUtil::sanitizeBundleContents); - public static final StreamCodec CONTAINER = codec(ItemContainerContents.STREAM_CODEC, contents -> ItemContainerContents.EMPTY); + private static StreamCodec codec(final StreamCodec delegate, final UnaryOperator sanitizer) { + return new DataSanitizationCodec<>(delegate, sanitizer); + } private static ChargedProjectiles sanitizeChargedProjectiles(final ChargedProjectiles projectiles) { if (projectiles.isEmpty()) { @@ -39,21 +50,26 @@ private static ChargedProjectiles sanitizeChargedProjectiles(final ChargedProjec } 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 sanitizedRepresentation = new ObjectArrayList<>(sizeUsed / 64 + 1); while (sizeUsed > 0) { @@ -66,20 +82,19 @@ private static BundleContents sanitizeBundleContents(final BundleContents conten return new BundleContents(sanitizedRepresentation); } - private static StreamCodec codec(final StreamCodec delegate, final UnaryOperator sanitizer) { - return new DataSanitizationCodec<>(delegate, sanitizer); - } - - private record DataSanitizationCodec(StreamCodec delegate, UnaryOperator sanitizer) implements StreamCodec { + // Codec used to override encoding if sanitization is enabled + private record DataSanitizationCodec(StreamCodec delegate, + UnaryOperator sanitizer) implements StreamCodec { @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 void encode(final @NonNull B buf, final @NonNull A value) { } } - 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() { - } } diff --git a/paper-server/src/main/java/io/papermc/paper/util/SafeAutoClosable.java b/paper-server/src/main/java/io/papermc/paper/util/SafeAutoClosable.java new file mode 100644 index 000000000000..4bc1c5853cec --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/SafeAutoClosable.java @@ -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(); +} diff --git a/paper-server/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java b/paper-server/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java index 0396589795da..355dcdb7cc5d 100644 --- a/paper-server/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java +++ b/paper-server/src/test/java/io/papermc/paper/configuration/GlobalConfigTestingBase.java @@ -1,14 +1,15 @@ package io.papermc.paper.configuration; +import net.minecraft.core.RegistryAccess; import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.serialize.SerializationException; public final class GlobalConfigTestingBase { - public static void setupGlobalConfigForTest() { + public static void setupGlobalConfigForTest(RegistryAccess registryAccess) { //noinspection ConstantConditions if (GlobalConfiguration.get() == null) { - ConfigurationNode node = PaperConfigurations.createForTesting(); + ConfigurationNode node = PaperConfigurations.createForTesting(registryAccess); try { GlobalConfiguration globalConfiguration = node.require(GlobalConfiguration.class); GlobalConfiguration.set(globalConfiguration); diff --git a/paper-server/src/test/java/org/bukkit/support/DummyServerHelper.java b/paper-server/src/test/java/org/bukkit/support/DummyServerHelper.java index 309d371247ad..78c0d08f6c76 100644 --- a/paper-server/src/test/java/org/bukkit/support/DummyServerHelper.java +++ b/paper-server/src/test/java/org/bukkit/support/DummyServerHelper.java @@ -91,7 +91,7 @@ public static Server setup() { when(instance.getPluginManager()).thenReturn(pluginManager); // Paper end - testing additions - io.papermc.paper.configuration.GlobalConfigTestingBase.setupGlobalConfigForTest(); // Paper - configuration files - setup global configuration test base + io.papermc.paper.configuration.GlobalConfigTestingBase.setupGlobalConfigForTest(RegistryHelper.getRegistry()); // Paper - configuration files - setup global configuration test base // Paper start - add test for recipe conversion when(instance.recipeIterator()).thenAnswer(ignored ->