mirror of
https://github.com/PaperMC/Paper.git
synced 2024-12-28 15:30:19 +01:00
8c5b837e05
Firstly, the old methods all routed to the CompletableFuture method. However, the CF method could not guarantee that if the caller was off-main that the future would be "completed" on-main. Since the callback methods used the CF one, this meant that the callback methods did not guarantee that the callbacks were to be called on the main thread. Now, all methods route to getChunkAtAsync(x, z, gen, urgent, cb) so that the methods with the callback are guaranteed to invoke the callback on the main thread. The CF behavior remains unchanged; it may still appear to complete on main if invoked off-main. Secondly, remove the scheduleOnMain invocation in the async chunk completion. This unnecessarily delays the callback by 1 tick. Thirdly, add getChunksAtAsync(minX, minZ, maxX, maxZ, ...) which will load chunks within an area. This method is provided as a helper as keeping all chunks loaded within an area can be complicated to implement for plugins (due to the lacking ticket API), and is already implemented internally anyways. Fourthly, remove the ticket addition that occured with getChunkAt and getChunkAtAsync. The ticket addition may delay the unloading of the chunk unnecessarily. It also fixes a very rare timing bug where the future/callback would be completed after the chunk unloads.
236 lines
14 KiB
Diff
236 lines
14 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Nassim Jahnke <nassim@njahnke.dev>
|
|
Date: Wed, 1 Dec 2021 12:36:25 +0100
|
|
Subject: [PATCH] Prevent sending oversized item data in equipment and metadata
|
|
|
|
Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com>
|
|
|
|
diff --git a/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java b/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..72483dedd3b1864fca3463d3ba3f6fad22680b97
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java
|
|
@@ -0,0 +1,108 @@
|
|
+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 net.minecraft.network.RegistryFriendlyByteBuf;
|
|
+import net.minecraft.network.codec.StreamCodec;
|
|
+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 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 {
|
|
+
|
|
+ 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);
|
|
+ public static final StreamCodec<RegistryFriendlyByteBuf, ItemContainerContents> CONTAINER = codec(ItemContainerContents.STREAM_CODEC, contents -> ItemContainerContents.EMPTY);
|
|
+
|
|
+ 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<ItemStack> 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 <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> {
|
|
+
|
|
+ @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().value().get()) {
|
|
+ this.delegate.encode(buf, value);
|
|
+ } else {
|
|
+ this.delegate.encode(buf, this.sanitizer.apply(value));
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public record DataSanitizer(AtomicBoolean value) implements AutoCloseable {
|
|
+
|
|
+ public DataSanitizer() {
|
|
+ this(new AtomicBoolean(false));
|
|
+ }
|
|
+
|
|
+ public void start() {
|
|
+ this.value.compareAndSet(false, true);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void close() {
|
|
+ this.value.compareAndSet(true, false);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private DataSanitizationUtil() {
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/net/minecraft/core/component/DataComponents.java b/src/main/java/net/minecraft/core/component/DataComponents.java
|
|
index a7f6a53cd1168bfc53b616cb0586a546fb9793e7..5e3bfd4a06b4a547a5f813f3d0c533fb7f4451f3 100644
|
|
--- a/src/main/java/net/minecraft/core/component/DataComponents.java
|
|
+++ b/src/main/java/net/minecraft/core/component/DataComponents.java
|
|
@@ -180,10 +180,10 @@ public class DataComponents {
|
|
"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 +250,7 @@ public class DataComponents {
|
|
"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()
|
|
diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java
|
|
index 59c1c103545f04fd35e6932df64a9910a1d74cd7..56bde49e6b7790155b032d0be40961d566ab89e9 100644
|
|
--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java
|
|
+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java
|
|
@@ -19,9 +19,11 @@ public record ClientboundSetEntityDataPacket(int id, List<SynchedEntityData.Data
|
|
}
|
|
|
|
private static void pack(List<SynchedEntityData.DataValue<?>> trackedValues, RegistryFriendlyByteBuf buf) {
|
|
+ try (var ignored = io.papermc.paper.util.DataSanitizationUtil.start(true)) { // Paper - data sanitization
|
|
for (SynchedEntityData.DataValue<?> dataValue : trackedValues) {
|
|
dataValue.write(buf);
|
|
}
|
|
+ } // Paper - data sanitization
|
|
|
|
buf.writeByte(255);
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java
|
|
index e46030178b3b54168a3532def308a08b10888723..830bd76916e26a3a54954d3cf7b7520af52a2258 100644
|
|
--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java
|
|
+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java
|
|
@@ -19,6 +19,13 @@ public class ClientboundSetEquipmentPacket implements Packet<ClientGamePacketLis
|
|
private final List<Pair<EquipmentSlot, ItemStack>> slots;
|
|
|
|
public ClientboundSetEquipmentPacket(int entityId, List<Pair<EquipmentSlot, ItemStack>> equipmentList) {
|
|
+ // Paper start - data sanitization
|
|
+ this(entityId, equipmentList, false);
|
|
+ }
|
|
+ private boolean sanitize;
|
|
+ public ClientboundSetEquipmentPacket(int entityId, List<Pair<EquipmentSlot, ItemStack>> equipmentList, boolean sanitize) {
|
|
+ this.sanitize = sanitize;
|
|
+ // Paper end - data sanitization
|
|
this.entity = entityId;
|
|
this.slots = equipmentList;
|
|
}
|
|
@@ -40,6 +47,7 @@ public class ClientboundSetEquipmentPacket implements Packet<ClientGamePacketLis
|
|
buf.writeVarInt(this.entity);
|
|
int i = this.slots.size();
|
|
|
|
+ try (var ignored = io.papermc.paper.util.DataSanitizationUtil.start(this.sanitize)) { // Paper - data sanitization
|
|
for (int j = 0; j < i; j++) {
|
|
Pair<EquipmentSlot, ItemStack> pair = this.slots.get(j);
|
|
EquipmentSlot equipmentSlot = pair.getFirst();
|
|
@@ -48,6 +56,7 @@ public class ClientboundSetEquipmentPacket implements Packet<ClientGamePacketLis
|
|
buf.writeByte(bl ? k | -128 : k);
|
|
ItemStack.OPTIONAL_STREAM_CODEC.encode(buf, pair.getSecond());
|
|
}
|
|
+ } // Paper - data sanitization
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
|
|
index f3456aeeab7eee5b6d0383a4bf1338dd8cc95bb3..b2fd3e936559c8fcb8b02ae3ef63c4f3bd0edb08 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerEntity.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
|
|
@@ -388,7 +388,7 @@ public class ServerEntity {
|
|
}
|
|
|
|
if (!list.isEmpty()) {
|
|
- sender.accept(new ClientboundSetEquipmentPacket(this.entity.getId(), list));
|
|
+ sender.accept(new ClientboundSetEquipmentPacket(this.entity.getId(), list, true)); // Paper - data sanitization
|
|
}
|
|
((LivingEntity) this.entity).detectEquipmentUpdatesPublic(); // CraftBukkit - SPIGOT-3789: sync again immediately after sending
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
index 688916c8fef40d4c81379ad38609a97993b4b702..6cf3b28749d92b4e33e2f88c6335c9a663edd534 100644
|
|
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
|
|
@@ -2752,7 +2752,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
|
|
entity.refreshEntityData(ServerGamePacketListenerImpl.this.player);
|
|
// SPIGOT-7136 - Allays
|
|
if (entity instanceof Allay || entity instanceof net.minecraft.world.entity.animal.horse.AbstractHorse) { // Paper - Fix horse armor desync
|
|
- ServerGamePacketListenerImpl.this.send(new ClientboundSetEquipmentPacket(entity.getId(), Arrays.stream(net.minecraft.world.entity.EquipmentSlot.values()).map((slot) -> Pair.of(slot, ((LivingEntity) entity).getItemBySlot(slot).copy())).collect(Collectors.toList())));
|
|
+ ServerGamePacketListenerImpl.this.send(new ClientboundSetEquipmentPacket(entity.getId(), Arrays.stream(net.minecraft.world.entity.EquipmentSlot.values()).map((slot) -> Pair.of(slot, ((LivingEntity) entity).getItemBySlot(slot).copy())).collect(Collectors.toList()), true)); // Paper - sanitize
|
|
}
|
|
|
|
ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote(); // Paper - fix slot desync - always refresh player inventory
|
|
diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
|
|
index 555d1b05ef6278567de598488b9486db965b8587..e33130ca8f3d2572018dd782b366af2989a95fd6 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
|
|
@@ -3461,7 +3461,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
|
|
}
|
|
|
|
});
|
|
- ((ServerLevel) this.level()).getChunkSource().broadcast(this, new ClientboundSetEquipmentPacket(this.getId(), list));
|
|
+ ((ServerLevel) this.level()).getChunkSource().broadcast(this, new ClientboundSetEquipmentPacket(this.getId(), list, true)); // Paper - data sanitization
|
|
}
|
|
|
|
private ItemStack getLastArmorItem(EquipmentSlot slot) {
|