rework ContraptionData

This commit is contained in:
TropheusJ 2025-02-09 23:37:02 -05:00
parent fe77abaf6e
commit 98698ddabd
7 changed files with 149 additions and 99 deletions

View file

@ -18,13 +18,13 @@ import com.simibubi.create.AllItems;
import com.simibubi.create.AllMovementBehaviours;
import com.simibubi.create.AllPackets;
import com.simibubi.create.AllSoundEvents;
import com.simibubi.create.Create;
import com.simibubi.create.content.contraptions.actors.psi.PortableStorageInterfaceMovement;
import com.simibubi.create.content.contraptions.actors.seat.SeatBlock;
import com.simibubi.create.content.contraptions.actors.seat.SeatEntity;
import com.simibubi.create.content.contraptions.actors.trainControls.ControlsStopControllingPacket;
import com.simibubi.create.content.contraptions.behaviour.MovementBehaviour;
import com.simibubi.create.content.contraptions.behaviour.MovementContext;
import com.simibubi.create.content.contraptions.data.ContraptionSyncLimiting;
import com.simibubi.create.content.contraptions.elevator.ElevatorContraption;
import com.simibubi.create.content.contraptions.glue.SuperGlueEntity;
import com.simibubi.create.content.contraptions.mounted.MountedContraption;
@ -615,11 +615,8 @@ public abstract class AbstractContraptionEntity extends Entity implements IEntit
CompoundTag compound = new CompoundTag();
writeAdditional(compound, true);
if (ContraptionData.isTooLargeForSync(compound)) {
String info = getContraption().getType().id + " @" + position() + " (" + getStringUUID() + ")";
Create.LOGGER.warn("Could not send Contraption Spawn Data (Packet too big): " + info);
compound = null;
}
if (ContraptionSyncLimiting.isTooLargeForSync(compound))
compound = null; // don't sync contraption data
buffer.writeNbt(compound);
}

View file

@ -1,85 +0,0 @@
package com.simibubi.create.content.contraptions;
import com.simibubi.create.compat.Mods;
import com.simibubi.create.foundation.mixin.accessor.NbtAccounterAccessor;
import com.simibubi.create.infrastructure.config.AllConfigs;
import io.netty.buffer.Unpooled;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtAccounter;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
public class ContraptionData {
/**
* A sane, default maximum for contraption data size.
*/
public static final int DEFAULT_LIMIT = 2_000_000;
/**
* Connectivity expands the NBT packet limit to 2 GB.
*/
public static final int CONNECTIVITY_LIMIT = Integer.MAX_VALUE;
/**
* Packet Fixer expands the NBT packet limit to 200 MB.
*/
public static final int PACKET_FIXER_LIMIT = 209_715_200;
/**
* XL Packets expands the NBT packet limit to 2 GB.
*/
public static final int XL_PACKETS_LIMIT = 2_000_000_000;
/**
* Minecart item sizes are limited by the vanilla slot change packet ({@link ClientboundContainerSetSlotPacket}).
* {@link #DEFAULT_LIMIT} is used as the default.
* Connectivity, PacketFixer, and XL Packets expand the size limit.
* If one of these mods is loaded, we take advantage of it and use the higher limit.
*/
public static final int PICKUP_LIMIT;
static {
int limit = DEFAULT_LIMIT;
// Check from largest to smallest to use the smallest limit if multiple mods are loaded.
// It is necessary to use the smallest limit because even if multiple mods are loaded,
// not all of their mixins may be applied. Therefore, it is safest to only assume that
// the mod with the smallest limit is actually active.
if (Mods.CONNECTIVITY.isLoaded()) {
limit = CONNECTIVITY_LIMIT;
}
if (Mods.XLPACKETS.isLoaded()) {
limit = XL_PACKETS_LIMIT;
}
if (Mods.PACKETFIXER.isLoaded()) {
limit = PACKET_FIXER_LIMIT;
}
PICKUP_LIMIT = limit;
}
/**
* @return true if the given NBT is too large for a contraption to be synced to clients.
*/
public static boolean isTooLargeForSync(CompoundTag data) {
int max = AllConfigs.server().kinetics.maxDataSize.get();
return max != 0 && packetSize(data) > max;
}
/**
* @return true if the given NBT is too large for a contraption to be picked up with a wrench.
*/
public static boolean isTooLargeForPickup(CompoundTag data) {
return packetSize(data) > PICKUP_LIMIT;
}
/**
* @return the size of the given NBT when put through a packet, in bytes.
*/
public static long packetSize(CompoundTag data) {
FriendlyByteBuf test = new FriendlyByteBuf(Unpooled.buffer());
test.writeNbt(data);
NbtAccounter sizeTracker = new NbtAccounter(Long.MAX_VALUE);
test.readNbt(sizeTracker);
long size = ((NbtAccounterAccessor) sizeTracker).create$getUsage();
test.release();
return size;
}
}

View file

@ -0,0 +1,55 @@
package com.simibubi.create.content.contraptions.data;
import com.simibubi.create.compat.Mods;
import com.simibubi.create.foundation.mixin.accessor.NbtAccounterAccessor;
import io.netty.buffer.Unpooled;
import net.minecraft.Util;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtAccounter;
import net.minecraft.network.FriendlyByteBuf;
public class ContraptionPickupLimiting {
/// The default NBT limit, defined by {@link FriendlyByteBuf#readNbt()}.
public static final int NBT_LIMIT = 2_097_152;
// increased nbt limits provided by other mods.
public static final int PACKET_FIXER_LIMIT = NBT_LIMIT * 100;
public static final int XL_PACKETS_LIMIT = Integer.MAX_VALUE;
// leave some space for the rest of the packet.
public static final int BUFFER = 20_000;
// the actual limit to be used
public static final int LIMIT = Util.make(() -> {
// the smallest limit needs to be used, as we can't guarantee that all mixins are applied if multiple are present.
if (Mods.PACKETFIXER.isLoaded()) {
return PACKET_FIXER_LIMIT;
} else if (Mods.XLPACKETS.isLoaded()) {
return XL_PACKETS_LIMIT;
}
// none are present, use vanilla default
return NBT_LIMIT;
}) - BUFFER;
/**
* @return true if the given NBT is too large for a contraption to be picked up with a wrench.
*/
public static boolean isTooLargeForPickup(CompoundTag data) {
return nbtSize(data) > LIMIT;
}
/**
* @return the size of the given NBT when read by the client according to {@link NbtAccounter}
*/
private static long nbtSize(CompoundTag data) {
FriendlyByteBuf test = new FriendlyByteBuf(Unpooled.buffer());
test.writeNbt(data);
NbtAccounter sizeTracker = new NbtAccounter(Long.MAX_VALUE);
test.readNbt(sizeTracker);
long size = ((NbtAccounterAccessor) sizeTracker).create$getUsage();
test.release();
return size;
}
}

View file

@ -0,0 +1,52 @@
package com.simibubi.create.content.contraptions.data;
import com.simibubi.create.compat.Mods;
import io.netty.buffer.Unpooled;
import net.minecraft.Util;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
public class ContraptionSyncLimiting {
/**
* Contraption entity sync is limited by the clientbound custom payload limit, since that's what Forge's
* extended spawn packet uses. The NBT limit is irrelevant since it's bypassed on deserialization.
*/
public static final int SIZE_LIMIT = 1_048_576;
// increased packet limits provided by other mods.
public static final int PACKET_FIXER_LIMIT = SIZE_LIMIT * 100;
public static final int XL_PACKETS_LIMIT = Integer.MAX_VALUE;
// leave some room for the rest of the packet.
public static final int BUFFER = 20_000;
// the actual limit to be used
public static final int LIMIT = Util.make(() -> {
// the smallest limit needs to be used, as we can't guarantee that all mixins are applied if multiple are present.
if (Mods.PACKETFIXER.isLoaded()) {
return PACKET_FIXER_LIMIT;
} else if (Mods.XLPACKETS.isLoaded()) {
return XL_PACKETS_LIMIT;
}
// none are present, use vanilla default
return SIZE_LIMIT;
}) - BUFFER;
/**
* @return true if the given NBT is too large for a contraption to be synced to clients.
*/
public static boolean isTooLargeForSync(CompoundTag data) {
return byteSize(data) > LIMIT;
}
/**
* @return the size of the given NBT when encoded, in bytes
*/
private static long byteSize(CompoundTag data) {
FriendlyByteBuf test = new FriendlyByteBuf(Unpooled.buffer());
test.writeNbt(data);
return test.writerIndex();
}
}

View file

@ -0,0 +1,37 @@
# Data Limiting
This pair of classes prevents clients from getting chunkbanned when contraptions are too large.
This information is up-to-date as of 1.20.1.
There's a few different packet limits in play:
- the NBT limit: `2_097_152`, enforced by `FriendlyByteBuf.readNbt()`
- the clientbound custom payload limit: `1_048_576` bytes, applies to `ClientboundCustomPayloadPacket`
- the serverbound custom payload limit: `32767` bytes, applies to `ServerboundCustomPayloadPacket`
- the packet limit: `8_388_608` bytes, applies to all packets
# NBT Size vs Bytes
There's two units in play as well - NBT Size and Bytes. The NBT limit uses NBT size, while the other
three use bytes. I'm (TropheusJ) not sure what exactly an NBT Size unit is - it's not bits, but it's
close.
Because of this discrepancy, the NBT limit is actually much lower than it seems. It will usually be
the first limit hit.
Bytes are found by writing a tag to a buffer and getting its `writerIndex`. NBT Size is found using
an `NbtAccounter`.
# Sync
Sync is pretty straightforward.
The only limit relevant here is the clientbound custom payload limit. The NBT limit would be relevant, but
client-side deserialization bypasses it.
Sync is much less of an issue compared to pickup, since a lot of data can be skipped when syncing.
# Pickup
Two limits are relevant for pickup: the NBT limit and the packet limit.
The NBT limit is hit way sooner, and is usually the limiting factor. Other mods may increase it, in
which case the packet limit may become relevant.
The custom payload limit is not relevant since item sync goes through the vanilla `ClientboundContainerSetSlotPacket`.

View file

@ -10,11 +10,11 @@ import com.simibubi.create.AllItems;
import com.simibubi.create.AllMovementBehaviours;
import com.simibubi.create.content.contraptions.AbstractContraptionEntity;
import com.simibubi.create.content.contraptions.Contraption;
import com.simibubi.create.content.contraptions.ContraptionData;
import com.simibubi.create.content.contraptions.ContraptionMovementSetting;
import com.simibubi.create.content.contraptions.OrientedContraptionEntity;
import com.simibubi.create.content.contraptions.actors.psi.PortableStorageInterfaceMovement;
import com.simibubi.create.content.contraptions.behaviour.MovementContext;
import com.simibubi.create.content.contraptions.data.ContraptionPickupLimiting;
import com.simibubi.create.content.kinetics.deployer.DeployerFakePlayer;
import com.simibubi.create.foundation.advancement.AllAdvancements;
import com.simibubi.create.foundation.utility.CreateLang;
@ -249,7 +249,7 @@ public class MinecartContraptionItem extends Item {
ItemStack generatedStack = create(type, oce).setHoverName(entity.getCustomName());
if (ContraptionData.isTooLargeForPickup(generatedStack.serializeNBT())) {
if (ContraptionPickupLimiting.isTooLargeForPickup(generatedStack.serializeNBT())) {
MutableComponent message = CreateLang.translateDirect("contraption.minecart_contraption_too_big")
.withStyle(ChatFormatting.RED);
player.displayClientMessage(message, true);

View file

@ -1,6 +1,5 @@
package com.simibubi.create.infrastructure.config;
import com.simibubi.create.content.contraptions.ContraptionData;
import com.simibubi.create.content.contraptions.ContraptionMovementSetting;
import net.createmod.catnip.config.ConfigBase;
@ -32,8 +31,6 @@ public class CKinetics extends ConfigBase {
public final ConfigGroup contraptions = group(1, "contraptions", "Moving Contraptions");
public final ConfigInt maxBlocksMoved = i(2048, 1, "maxBlocksMoved", Comments.maxBlocksMoved);
public final ConfigInt maxDataSize =
i(ContraptionData.DEFAULT_LIMIT, 0, "maxDataSize", Comments.bytes, Comments.maxDataDisable, Comments.maxDataSize, Comments.maxDataSize2);
public final ConfigInt maxChassisRange = i(16, 1, "maxChassisRange", Comments.maxChassisRange);
public final ConfigInt maxPistonPoles = i(64, 1, "maxPistonPoles", Comments.maxPistonPoles);
public final ConfigInt maxRopeLength = i(384, 1, "maxRopeLength", Comments.maxRopeLength);
@ -86,9 +83,6 @@ public class CKinetics extends ConfigBase {
"multiplier used for calculating exhaustion from speed when a crank is turned.";
static String maxBlocksMoved =
"Maximum amount of blocks in a structure movable by Pistons, Bearings or other means.";
static String maxDataSize = "Maximum amount of data a contraption can have before it can't be synced with players.";
static String maxDataSize2 = "Un-synced contraptions will not be visible and will not have collision.";
static String maxDataDisable = "[0 to disable this limit]";
static String maxChassisRange = "Maximum value of a chassis attachment range.";
static String maxPistonPoles = "Maximum amount of extension poles behind a Mechanical Piston.";
static String maxRopeLength = "Max length of rope available off a Rope Pulley.";