Push initial datacomponent obfuscation logic

This general principal is that there is no mutation of the patched map, only swapping of the valid values.

Additional work will need to be done to support hiding enchantments whilst keeping the glint, as we either have to append an enchantment, or need to add a glint override.. which kinda defeats the purpose above. Cant add a vanilla enchantment because those can be removed! Further investigation needed.
This commit is contained in:
Owen1212055 2024-12-23 20:17:50 -05:00
parent aa2c52baac
commit 37a20964c3
No known key found for this signature in database
GPG key ID: 2133292072886A30
8 changed files with 213 additions and 54 deletions

View file

@ -0,0 +1,25 @@
--- a/net/minecraft/core/component/DataComponentType.java
+++ b/net/minecraft/core/component/DataComponentType.java
@@ -80,11 +_,11 @@
static class SimpleType<T> implements DataComponentType<T> {
@Nullable
private final Codec<T> codec;
- private final StreamCodec<? super RegistryFriendlyByteBuf, T> streamCodec;
+ private final java.util.function.Supplier<StreamCodec<? super RegistryFriendlyByteBuf, T>> streamCodec; // Paper
SimpleType(@Nullable Codec<T> codec, StreamCodec<? super RegistryFriendlyByteBuf, T> streamCodec) {
this.codec = codec;
- this.streamCodec = streamCodec;
+ this.streamCodec = com.google.common.base.Suppliers.memoize(() -> io.papermc.paper.util.DataSanitizationUtil.get(this, streamCodec)); // Paper
}
@Nullable
@@ -95,7 +_,7 @@
@Override
public StreamCodec<? super RegistryFriendlyByteBuf, T> streamCodec() {
- return this.streamCodec;
+ return this.streamCodec.get(); // Paper
}
@Override

View file

@ -1,24 +0,0 @@
--- a/net/minecraft/core/component/DataComponents.java
+++ b/net/minecraft/core/component/DataComponents.java
@@ -180,10 +_,10 @@
"map_post_processing", builder -> builder.networkSynchronized(MapPostProcessing.STREAM_CODEC)
);
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
);
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
);
public static final DataComponentType<PotionContents> POTION_CONTENTS = register(
"potion_contents", builder -> builder.persistent(PotionContents.CODEC).networkSynchronized(PotionContents.STREAM_CODEC).cacheEncoding()
@@ -250,7 +_,7 @@
"pot_decorations", builder -> builder.persistent(PotDecorations.CODEC).networkSynchronized(PotDecorations.STREAM_CODEC).cacheEncoding()
);
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
);
public static final DataComponentType<BlockItemStateProperties> BLOCK_STATE = register(
"block_state", builder -> builder.persistent(BlockItemStateProperties.CODEC).networkSynchronized(BlockItemStateProperties.STREAM_CODEC).cacheEncoding()

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.DataSanitizationUtil.sanitizeCount(value, value.getCount())); // Paper
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 (io.papermc.paper.util.DataSanitizationUtil.SafeAutoClosable unused = io.papermc.paper.util.DataSanitizationUtil.passContext(value)) {
+ 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

@ -7,8 +7,13 @@ import io.papermc.paper.configuration.type.number.DoubleOr;
import io.papermc.paper.configuration.type.number.IntOr;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minecraft.Util;
import net.minecraft.core.component.DataComponentType;
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.ItemStack;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
@ -16,10 +21,13 @@ import org.spongepowered.configurate.objectmapping.meta.Comment;
import org.spongepowered.configurate.objectmapping.meta.PostProcess;
import org.spongepowered.configurate.objectmapping.meta.Required;
import org.spongepowered.configurate.objectmapping.meta.Setting;
import org.spongepowered.configurate.serialize.SerializationException;
import java.util.HashSet;
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 {
@ -354,4 +362,68 @@ 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 = new Items();
public class Items extends ConfigurationPart {
public static Set<DataComponentType<?>> OVERRIDEN_TYPES = new HashSet<>();
public boolean enableItemObfuscation = false;
public AssetObfuscationConfiguration allModels = new AssetObfuscationConfiguration(true,
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.ATTRIBUTE_MODIFIERS,
DataComponents.CUSTOM_DATA,
DataComponents.WRITABLE_BOOK_CONTENT,
DataComponents.WRITTEN_BOOK_CONTENT,
DataComponents.MAP_ID,
DataComponents.MAP_DECORATIONS,
DataComponents.MAP_POST_PROCESSING,
DataComponents.LODESTONE_TRACKER
)
);
public Map<String, AssetObfuscationConfiguration> modelOverrides = Map.of(
net.minecraft.world.item.Items.ELYTRA.components().get(DataComponents.ITEM_MODEL).toString(), new AssetObfuscationConfiguration(
true, Util.make(new HashSet<>(), (obj) -> {
obj.addAll(allModels.sanitizedComponents());
obj.remove(DataComponents.DAMAGE); // don't sanitize damage for elytra
}
)
));
public AssetObfuscationConfiguration getAssetObfuscation(ItemStack resourceLocation) {
return this.modelOverrides.getOrDefault(resourceLocation.get(DataComponents.ITEM_MODEL).toString(), this.allModels); // todo make optimized resourcelocation lookup
}
@ConfigSerializable
public record AssetObfuscationConfiguration(boolean sanitizeCount, Set<DataComponentType<?>> sanitizedComponents) {
}
@PostProcess
public void computeOverridenTypes() {
OVERRIDEN_TYPES.addAll(this.allModels.sanitizedComponents);
for (AssetObfuscationConfiguration configuration : this.modelOverrides.values()) {
OVERRIDEN_TYPES.addAll(configuration.sanitizedComponents);
}
}
}
}
}
}

View file

@ -48,6 +48,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;
@ -193,16 +194,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 +318,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()));
@ -456,7 +458,7 @@ public class PaperConfigurations extends Configurations<GlobalConfiguration, Wor
@VisibleForTesting
static ConfigurationNode createForTesting() {
ObjectMapper.Factory factory = defaultGlobalFactoryBuilder(ObjectMapper.factoryBuilder()).build();
ConfigurationOptions options = defaultGlobalOptions(defaultOptions(ConfigurationOptions.defaults()))
ConfigurationOptions options = defaultGlobalOptions(null, defaultOptions(ConfigurationOptions.defaults())) // TODO: ???
.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

@ -1,19 +1,34 @@
package io.papermc.paper.util;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
import com.google.common.base.Preconditions;
import io.papermc.paper.configuration.GlobalConfiguration;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import net.minecraft.world.item.ItemStack;
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 net.minecraft.world.item.component.LodestoneTracker;
import net.minecraft.world.item.enchantment.ItemEnchantments;
import net.minecraft.world.level.Level;
import org.apache.commons.lang3.math.Fraction;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
@DefaultQualifier(NonNull.class)
@ -29,9 +44,41 @@ public final class DataSanitizationUtil {
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);
public static final StreamCodec<RegistryFriendlyByteBuf, ItemContainerContents> CONTAINER = codec(ItemContainerContents.STREAM_CODEC, contents -> ItemContainerContents.EMPTY);
public static SafeAutoClosable passContext(final ItemStack itemStack) {
final DataSanitizer sanitizer = DATA_SANITIZER.get();
if (!sanitizer.isSanitizing()) {
return () -> {}; // Dont pass any context
}
return new ContentScope(sanitizer.scope.get(), itemStack);
}
public static final Map<DataComponentType<?>, StreamCodec<? super RegistryFriendlyByteBuf, ?>> BUILT_IN_CODEC_OVERRIDES = Map.of(
DataComponents.CHARGED_PROJECTILES, codec(ChargedProjectiles.STREAM_CODEC, DataSanitizationUtil::sanitizeChargedProjectiles),
DataComponents.BUNDLE_CONTENTS, codec(BundleContents.STREAM_CODEC, DataSanitizationUtil::sanitizeBundleContents),
DataComponents.CONTAINER, codec(ItemContainerContents.STREAM_CODEC, contents -> ItemContainerContents.EMPTY)
);
public static final Map<DataComponentType<?>, Object> EMPTY_MAP = Map.of(
DataComponents.LODESTONE_TRACKER, new LodestoneTracker(Optional.empty(), false)
);
public static <T> StreamCodec<? super RegistryFriendlyByteBuf, T> get(DataComponentType<T> componentType, StreamCodec<? super RegistryFriendlyByteBuf, T> vanillaCodec) {
// First check do we have our built in overrides, if so, override it.
if (BUILT_IN_CODEC_OVERRIDES.containsKey(componentType)) {
return (StreamCodec<RegistryFriendlyByteBuf, T>) BUILT_IN_CODEC_OVERRIDES.get(componentType);
}
// Now check the obfuscation, where we lookup if the type is overriden in any of the configurations, if so, wrap the codec
GlobalConfiguration.Anticheat.Obfuscation.Items obfuscation = GlobalConfiguration.get().anticheat.obfuscation.items;
if (obfuscation.enableItemObfuscation && GlobalConfiguration.Anticheat.Obfuscation.Items.OVERRIDEN_TYPES.contains(componentType)) {
return new DataSanitizationCodec<>(vanillaCodec, new DefaultDataComponentSanitizer<>(componentType, EMPTY_MAP.get(componentType)));
}
return vanillaCodec;
}
private static ChargedProjectiles sanitizeChargedProjectiles(final ChargedProjectiles projectiles) {
if (projectiles.isEmpty()) {
@ -65,11 +112,37 @@ public final class DataSanitizationUtil {
// Ensure that potentially overstacked bundles are not represented by empty (count=0) itemstacks.
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);
}
public static int sanitizeCount(ItemStack itemStack, int count) {
GlobalConfiguration.Anticheat.Obfuscation.Items items = GlobalConfiguration.get().anticheat.obfuscation.items;
if (items.enableItemObfuscation && items.getAssetObfuscation(itemStack).sanitizeCount()) {
return 1;
} else {
return count;
}
}
private record DefaultDataComponentSanitizer<A>(DataComponentType<?> type, @Nullable Object override) implements UnaryOperator<A> {
@Override
public A apply(final A oldvalue) {
ContentScope scope = DATA_SANITIZER.get().scope.get();
ItemStack targetItemstack = scope.itemStack;
// Does this asset override this component? If not, return oldValue.
if (!GlobalConfiguration.get().anticheat.obfuscation.items.getAssetObfuscation(targetItemstack).sanitizedComponents().contains(this.type)) {
return oldvalue;
}
// We don't need to check if its overriden because we KNOW it is if we are serializing it over to the client.
return (A) scope.itemStack.getPrototype().getOrDefault(this.type, this.override == null ? oldvalue : this.override);
}
}
private record DataSanitizationCodec<B, A>(StreamCodec<B, A> delegate, UnaryOperator<A> sanitizer) implements StreamCodec<B, A> {
@Override
@ -79,7 +152,7 @@ public final class DataSanitizationUtil {
@Override
public void encode(final @NonNull B buf, final @NonNull A value) {
if (!DATA_SANITIZER.get().value().get()) {
if (!DATA_SANITIZER.get().isSanitizing()) {
this.delegate.encode(buf, value);
} else {
this.delegate.encode(buf, this.sanitizer.apply(value));
@ -87,10 +160,10 @@ public final class DataSanitizationUtil {
}
}
public record DataSanitizer(AtomicBoolean value) implements AutoCloseable {
public record DataSanitizer(AtomicBoolean value, AtomicReference<ContentScope> scope) implements SafeAutoClosable {
public DataSanitizer() {
this(new AtomicBoolean(false));
this(new AtomicBoolean(false), new AtomicReference<>());
}
public void start() {
@ -101,6 +174,27 @@ public final class DataSanitizationUtil {
public void close() {
this.value.compareAndSet(true, false);
}
public boolean isSanitizing() {
return value().get();
}
}
public record ContentScope(@Nullable ContentScope oldScope, ItemStack itemStack) implements SafeAutoClosable {
public ContentScope {
Preconditions.checkNotNull(DATA_SANITIZER.get(), "Expected data santizier content available");
}
@Override
public void close() {
DATA_SANITIZER.get().scope().set(this.oldScope);
}
}
public interface SafeAutoClosable extends AutoCloseable {
@Override
void close();
}
private DataSanitizationUtil() {