diff --git a/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch index a71879d2eb..366957ed39 100644 --- a/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch @@ -134,7 +134,7 @@ LOGGER.info("Loading properties"); DedicatedServerProperties properties = this.settings.getProperties(); if (this.isSingleplayer()) { -@@ -132,13 +_,51 @@ +@@ -132,13 +_,52 @@ this.setLocalIp(properties.serverIp); } @@ -148,6 +148,7 @@ + this.paperConfigurations.initializeGlobalConfiguration(this.registryAccess()); + this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess()); + // Paper end - initialize global and world-defaults configuration ++ io.papermc.paper.util.DataSanitizationUtil.bindCodecs(); // Paper - bind codec network serializers for sanitization + this.server.spark.enableEarlyIfRequested(); // Paper - spark + // Paper start - fix converting txt to json file; convert old users earlier after PlayerList creation but before file load/save + if (this.convertOldUsers()) { 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 dde005a228..d70dd3da59 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,9 +5,9 @@ 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.DataSanitizationUtil; 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; @@ -21,8 +21,8 @@ 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.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; @@ -373,54 +373,52 @@ public class GlobalConfiguration extends ConfigurationPart { public Items items = new Items(); public class Items extends ConfigurationPart { - public static Set> OVERRIDEN_TYPES = new HashSet<>(); + public static Set> BASE_OVERRIDERS = Set.of( + DataComponents.MAX_STACK_SIZE, + DataComponents.MAX_DAMAGE, + DataComponents.DAMAGE, + //DataComponents.UNBREAKABLE, - we cant really do anything about tihs + 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.LODESTONE_TRACKER + ); 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 + Set.of(DataComponents.LODESTONE_TRACKER), + Set.of() + ); + + public Map modelOverrides = Map.of( + net.minecraft.world.item.Items.ELYTRA.components().get(DataComponents.ITEM_MODEL).toString(), new AssetObfuscationConfiguration(true, + Set.of(DataComponents.DAMAGE), + Set.of() ) ); - public Map 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> sanitizedComponents) { + public record AssetObfuscationConfiguration(@Required boolean sanitizeCount, Set> dontObfuscate, Set> alsoObfuscate) { + public AssetObfuscationConfiguration(final boolean sanitizeCount, final @Nullable Set> dontObfuscate, final @Nullable Set> alsoObfuscate) { + this.sanitizeCount = sanitizeCount; + this.dontObfuscate = Objects.requireNonNullElse(dontObfuscate, Set.of()); + this.alsoObfuscate = Objects.requireNonNullElse(alsoObfuscate, Set.of()); + } + } @PostProcess public void computeOverridenTypes() { - OVERRIDEN_TYPES.addAll(this.allModels.sanitizedComponents); - for (AssetObfuscationConfiguration configuration : this.modelOverrides.values()) { - OVERRIDEN_TYPES.addAll(configuration.sanitizedComponents); - } + DataSanitizationUtil.compute(this); } + } } diff --git a/paper-server/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java b/paper-server/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java index 91afe7001a..48245643a3 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java +++ b/paper-server/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java @@ -1,32 +1,21 @@ 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 java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; 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; @@ -34,7 +23,55 @@ import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public final class DataSanitizationUtil { - private static final ThreadLocal DATA_SANITIZER = ThreadLocal.withInitial(DataSanitizer::new); + static final ThreadLocal DATA_SANITIZER = ThreadLocal.withInitial(DataSanitizer::new); + + // + static Set> OVERRIDE_TYPES = new HashSet<>(); + static BoundObfuscationConfiguration BOUND_BASE = null; + static Map BOUND_OVERRIDES = new HashMap<>(); + // We need to have a special ignore item to indicate this item should not be obfuscated. + // Randomize the namespace to ensure servers dont try to utilize this. + static ResourceLocation IGNORE_OBFUSCATION_ITEM = ResourceLocation.tryParse("paper:ignore_obfuscation_item/" + UUID.randomUUID()); + + public static void compute(GlobalConfiguration.Anticheat.Obfuscation.Items items) { + + OVERRIDE_TYPES.addAll(GlobalConfiguration.Anticheat.Obfuscation.Items.BASE_OVERRIDERS); + + // Add any possible new types obfuscated + OVERRIDE_TYPES.addAll(items.allModels.alsoObfuscate()); + for (GlobalConfiguration.Anticheat.Obfuscation.Items.AssetObfuscationConfiguration configuration : items.modelOverrides.values()) { + OVERRIDE_TYPES.addAll(configuration.alsoObfuscate()); + } + + // now bind them all + BOUND_BASE = bind(items.allModels); + for (Map.Entry entry : items.modelOverrides.entrySet()) { + BOUND_OVERRIDES.put(ResourceLocation.parse(entry.getKey()), bind(entry.getValue())); + } + BOUND_OVERRIDES.put(IGNORE_OBFUSCATION_ITEM, new BoundObfuscationConfiguration(false, Set.of())); + } + + public record BoundObfuscationConfiguration(boolean sanitizeCount, Set> sanitize) { + + } + + public static BoundObfuscationConfiguration bind(GlobalConfiguration.Anticheat.Obfuscation.Items.AssetObfuscationConfiguration config) { + Set> base = new HashSet<>(GlobalConfiguration.Anticheat.Obfuscation.Items.BASE_OVERRIDERS); + base.addAll(config.alsoObfuscate()); + base.removeAll(config.dontObfuscate()); + + return new BoundObfuscationConfiguration(config.sanitizeCount(), base); + } + + public static void bindCodecs() { + BuiltInRegistries.DATA_COMPONENT_TYPE.stream().forEach(DataComponentType::streamCodec); // populate the consumers + OVERRIDE_TYPES.clear(); // We dont need this anymore + } + // + + public static BoundObfuscationConfiguration getAssetObfuscation(ItemStack resourceLocation) { + return BOUND_OVERRIDES.getOrDefault(resourceLocation.get(DataComponents.ITEM_MODEL), BOUND_BASE); + } public static DataSanitizer start(final boolean sanitize) { final DataSanitizer sanitizer = DATA_SANITIZER.get(); @@ -46,118 +83,21 @@ public final class DataSanitizationUtil { public static SafeAutoClosable passContext(final ItemStack itemStack) { final DataSanitizer sanitizer = DATA_SANITIZER.get(); - if (!sanitizer.isSanitizing()) { + if (sanitizer.isNotSanitizing()) { return () -> {}; // Dont pass any context } - return new ContentScope(sanitizer.scope.get(), itemStack); + ContentScope closable = new ContentScope(sanitizer.scope.get(), itemStack);; + sanitizer.scope.set(closable); + return closable; } - - public static final Map, StreamCodec> 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, Object> EMPTY_MAP = Map.of( - DataComponents.LODESTONE_TRACKER, new LodestoneTracker(Optional.empty(), false) - ); - - public static StreamCodec get(DataComponentType componentType, StreamCodec vanillaCodec) { - // First check do we have our built in overrides, if so, override it. - if (BUILT_IN_CODEC_OVERRIDES.containsKey(componentType)) { - return (StreamCodec) 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()) { - return projectiles; - } - - return ChargedProjectiles.of(List.of( - new ItemStack(projectiles.contains(Items.FIREWORK_ROCKET) ? Items.FIREWORK_ROCKET : Items.ARROW) - )); - } - - 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)))); - - final List sanitizedRepresentation = new ObjectArrayList<>(sizeUsed / 64 + 1); - while (sizeUsed > 0) { - final int stackCount = Math.min(64, sizeUsed); - sanitizedRepresentation.add(new ItemStack(Items.PAPER, stackCount)); - sizeUsed -= stackCount; - } - // Now we add a single fake item that uses the same amount of slots as all other items. - // Ensure that potentially overstacked bundles are not represented by empty (count=0) itemstacks. - return new BundleContents(sanitizedRepresentation); - } - private static StreamCodec codec(final StreamCodec delegate, final UnaryOperator sanitizer) { - return new DataSanitizationCodec<>(delegate, sanitizer); + return ItemComponentSanitizer.get(componentType, vanillaCodec); } 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(DataComponentType type, @Nullable Object override) implements UnaryOperator { - - @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(StreamCodec delegate, UnaryOperator sanitizer) implements StreamCodec { - - @Override - public @NonNull A decode(final @NonNull B buf) { - return this.delegate.decode(buf); - } - - @Override - public void encode(final @NonNull B buf, final @NonNull A value) { - if (!DATA_SANITIZER.get().isSanitizing()) { - this.delegate.encode(buf, value); - } else { - this.delegate.encode(buf, this.sanitizer.apply(value)); - } - } + return ItemComponentSanitizer.sanitizeCount(itemStack, count); } public record DataSanitizer(AtomicBoolean value, AtomicReference scope) implements SafeAutoClosable { @@ -175,8 +115,8 @@ public final class DataSanitizationUtil { this.value.compareAndSet(true, false); } - public boolean isSanitizing() { - return value().get(); + public boolean isNotSanitizing() { + return !value().get(); } } 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 0000000000..35c2951836 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/ItemComponentSanitizer.java @@ -0,0 +1,231 @@ +package io.papermc.paper.util; + +import com.google.common.collect.ImmutableMap; +import io.papermc.paper.configuration.GlobalConfiguration; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import net.minecraft.Util; +import net.minecraft.core.HolderSet; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.LockCode; +import net.minecraft.world.item.AdventureModePredicate; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.PotionContents; +import net.minecraft.world.item.component.BundleContents; +import net.minecraft.world.item.component.ChargedProjectiles; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.component.DeathProtection; +import net.minecraft.world.item.component.ItemContainerContents; +import net.minecraft.world.item.component.LodestoneTracker; +import net.minecraft.world.item.component.OminousBottleAmplifier; +import net.minecraft.world.item.component.Tool; +import net.minecraft.world.item.component.UseCooldown; +import net.minecraft.world.item.component.UseRemainder; +import net.minecraft.world.item.component.WritableBookContent; +import net.minecraft.world.item.component.WrittenBookContent; +import net.minecraft.world.item.enchantment.ItemEnchantments; +import net.minecraft.world.item.enchantment.Repairable; +import net.minecraft.world.level.saveddata.maps.MapId; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class ItemComponentSanitizer { + + /* + These represent codecs that are meant to help get rid of possibly big items by ALWAYS hiding this data. + */ + public static final Map, StreamCodec> OVERSIZE_OVERRIDES = Map.of( + DataComponents.CHARGED_PROJECTILES, codec(ChargedProjectiles.STREAM_CODEC, ItemComponentSanitizer::sanitizeChargedProjectiles), + DataComponents.BUNDLE_CONTENTS, codec(BundleContents.STREAM_CODEC, ItemComponentSanitizer::sanitizeBundleContents), + DataComponents.CONTAINER, codec(ItemContainerContents.STREAM_CODEC, contents -> ItemContainerContents.EMPTY) + ); + + /* + * This map defines overrides for specific DataComponentType entries that should be sanitized. + * + * Components critical to item functionality are not sanitized. + * This map only applies to components that are patched ontop of items and should ignore its prototype. + * + * Any components not present in this map, the prototype value will be sent. + */ + public static final Map, UnaryOperator> SANITIZATION_OVERRIDES = Util.make(ImmutableMap., UnaryOperator>builder(), (map) -> { + put(map, DataComponents.LODESTONE_TRACKER, empty(new LodestoneTracker(Optional.empty(), false))); + put(map, DataComponents.ENCHANTMENTS, empty(dummyEnchantments())); + put(map, DataComponents.MAX_DAMAGE, empty(1)); + put(map, DataComponents.DAMAGE, empty(0)); + put(map, DataComponents.CUSTOM_NAME, empty(Component.empty())); + put(map, DataComponents.TOOLTIP_STYLE, empty(ResourceLocation.parse("paper:fake-tooltip"))); + put(map, DataComponents.DEATH_PROTECTION, empty(new DeathProtection(List.of()))); + put(map, DataComponents.STORED_ENCHANTMENTS, empty(dummyEnchantments())); + put(map, DataComponents.CAN_PLACE_ON, empty(new AdventureModePredicate(List.of(), true))); + put(map, DataComponents.REPAIR_COST, empty(0)); + put(map, DataComponents.USE_REMAINDER, empty(new UseRemainder(ItemStack.EMPTY))); + put(map, DataComponents.USE_COOLDOWN, empty(new UseCooldown(1, Optional.empty()))); + put(map, DataComponents.TOOL, empty(new Tool(List.of(), 1F, 1))); + put(map, DataComponents.REPAIRABLE, empty(new Repairable(HolderSet.empty()))); + put(map, DataComponents.CUSTOM_DATA, empty(CustomData.EMPTY)); + put(map, DataComponents.POTION_CONTENTS, ItemComponentSanitizer::sanitizePotionContents); + put(map, DataComponents.ENTITY_DATA, empty(CustomData.EMPTY)); + put(map, DataComponents.BUCKET_ENTITY_DATA, empty(CustomData.EMPTY)); + put(map, DataComponents.BLOCK_ENTITY_DATA, empty(CustomData.EMPTY)); + put(map, DataComponents.OMINOUS_BOTTLE_AMPLIFIER, empty(new OminousBottleAmplifier(0))); + put(map, DataComponents.BEES, empty(List.of())); + put(map, DataComponents.LOCK, empty(LockCode.NO_LOCK)); + put(map, DataComponents.WRITABLE_BOOK_CONTENT, empty(WritableBookContent.EMPTY)); + put(map, DataComponents.WRITTEN_BOOK_CONTENT, empty(WrittenBookContent.EMPTY)); + put(map, DataComponents.MAP_ID, empty(new MapId(0))); + } + ).build(); + + private static void put(ImmutableMap.Builder map, DataComponentType type, UnaryOperator object) { + map.put(type, object); + } + + private static UnaryOperator empty(T object) { + return (unused) -> object; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static StreamCodec get(DataComponentType componentType, StreamCodec vanillaCodec) { + // First check do we have our built in overrides, if so, override it. + if (OVERSIZE_OVERRIDES.containsKey(componentType)) { + return (StreamCodec) OVERSIZE_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 && DataSanitizationUtil.OVERRIDE_TYPES.contains(componentType)) { + return codec(vanillaCodec, new DefaultDataComponentSanitizer<>(componentType, (UnaryOperator) SANITIZATION_OVERRIDES.get(componentType))); + } + + return vanillaCodec; + } + + // + + 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() { + ItemEnchantments.Mutable obj = new ItemEnchantments.Mutable(ItemEnchantments.EMPTY); + obj.set(MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getRandom(RandomSource.create()).orElseThrow(), 1); + return obj.toImmutable(); + } + + private static ChargedProjectiles sanitizeChargedProjectiles(final ChargedProjectiles projectiles) { + if (projectiles.isEmpty()) { + return projectiles; + } + + return ChargedProjectiles.of(List.of( + new ItemStack(projectiles.contains(Items.FIREWORK_ROCKET) ? Items.FIREWORK_ROCKET : Items.ARROW) + )); + } + + private static final ItemStack BUNDLE_ITEM_FILLER = Util.make(new ItemStack(Items.PAPER, 1), (itemStack) -> { + if (GlobalConfiguration.get().anticheat.obfuscation.items.enableItemObfuscation) { + itemStack.set(DataComponents.ITEM_MODEL, DataSanitizationUtil.IGNORE_OBFUSCATION_ITEM); // Prevent this item from being obfuscated + } + }); + + // 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; + } + + // 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(BUNDLE_ITEM_FILLER.copyWithCount(Math.max(1, sizeUsed)))); + } + + final List sanitizedRepresentation = new ObjectArrayList<>(sizeUsed / 64 + 1); + while (sizeUsed > 0) { + final int stackCount = Math.min(64, sizeUsed); + sanitizedRepresentation.add(BUNDLE_ITEM_FILLER.copyWithCount(stackCount)); + sizeUsed -= stackCount; + } + // Now we add a single fake item that uses the same amount of slots as all other items. + // Ensure that potentially overstacked bundles are not represented by empty (count=0) itemstacks. + return new BundleContents(sanitizedRepresentation); + } + // + + public static int sanitizeCount(ItemStack itemStack, int count) { + GlobalConfiguration.Anticheat.Obfuscation.Items items = GlobalConfiguration.get().anticheat.obfuscation.items; + if (items.enableItemObfuscation && DataSanitizationUtil.getAssetObfuscation(itemStack).sanitizeCount()) { + return 1; + } else { + return count; + } + } + + private static StreamCodec codec(final StreamCodec delegate, final UnaryOperator sanitizer) { + return new DataSanitizationCodec<>(delegate, sanitizer); + } + + // Serializer that will override the type depending on the asset of the item provided + private record DefaultDataComponentSanitizer(DataComponentType type, + @Nullable UnaryOperator override) implements UnaryOperator { + + @Override + public A apply(final A oldvalue) { + DataSanitizationUtil.ContentScope scope = DataSanitizationUtil.DATA_SANITIZER.get().scope().get(); + ItemStack targetItemstack = scope.itemStack(); + // Does this asset override this component? If not, return oldValue. + if (!DataSanitizationUtil.getAssetObfuscation(targetItemstack).sanitize().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 this.override == null ? (A) scope.itemStack().getPrototype().getOrDefault(this.type, oldvalue) : this.override.apply(oldvalue); + } + } + + + // 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); + } + + @Override + public void encode(final @NonNull B buf, final @NonNull A value) { + if (DataSanitizationUtil.DATA_SANITIZER.get().isNotSanitizing()) { + this.delegate.encode(buf, value); + } else { + this.delegate.encode(buf, this.sanitizer.apply(value)); + } + } + } + +}