mirror of
https://github.com/PaperMC/Paper.git
synced 2025-01-18 23:23:19 +01:00
Reimplement ItemStack Obfuscation (#11817)
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:
parent
efdcaa25ee
commit
55f3f280cb
17 changed files with 524 additions and 80 deletions
paper-server
patches/sources/net/minecraft
core/component
network/protocol/game
world/item
src
main/java/io/papermc/paper
configuration
Configurations.javaGlobalConfiguration.javaPaperConfigurations.javaWorldConfiguration.java
serializer
util
test/java
|
@ -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 @@
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 ->
|
||||
|
|
Loading…
Reference in a new issue