1
0
Fork 0
mirror of https://github.com/PaperMC/Paper.git synced 2025-01-18 23:23:19 +01:00

Reimplement ItemStack Obfuscation ()

Reimplementation of the itemstack obfuscation config that
leverages the component patch map codec to drop
unwanted components on items or replaces them with
sanitized versions.

Co-authored-by: Bjarne Koll <git@lynxplay.dev>
Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com>
This commit is contained in:
Owen 2025-01-09 12:58:18 -05:00 committed by GitHub
parent efdcaa25ee
commit 55f3f280cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 524 additions and 80 deletions

View file

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

View file

@ -5,11 +5,11 @@
);
public static final DataComponentType<ChargedProjectiles> 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<BundleContents> 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<PotionContents> POTION_CONTENTS = register(
"potion_contents", builder -> builder.persistent(PotionContents.CODEC).networkSynchronized(PotionContents.STREAM_CODEC).cacheEncoding()
@ -18,7 +18,7 @@
);
public static final DataComponentType<ItemContainerContents> 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<BlockItemStateProperties> BLOCK_STATE = register(
"block_state", builder -> builder.persistent(BlockItemStateProperties.CODEC).networkSynchronized(BlockItemStateProperties.STREAM_CODEC).cacheEncoding()

View file

@ -4,7 +4,7 @@
}
private static void pack(List<SynchedEntityData.DataValue<?>> 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);
}

View file

@ -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<EquipmentSlot, ItemStack> pair = this.slots.get(i);
EquipmentSlot equipmentSlot = pair.getFirst();

View file

@ -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 {

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,14 @@ 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 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 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 +74,7 @@ public class GlobalConfiguration 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 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

@ -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 {

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

@ -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<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.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 <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();
}

View file

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

View file

@ -91,7 +91,7 @@ public final class DummyServerHelper {
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 ->