Fix lots of vanilla teleportation-related bugs

This commit is contained in:
Runemoro 2018-04-24 20:02:42 -04:00
parent 7a5d7aecb6
commit 5095518b44
10 changed files with 231 additions and 113 deletions

View file

@ -74,12 +74,13 @@ minecraft {
replace '${version}', fullVersion
makeObfSourceJar = false
//noinspection GroovyUnusedAssignment
def args = [ // TODO: https://github.com/SpongePowered/Mixin/issues/140
def args = [
"-Dfml.noGrab=false",
"-Dfml.coreMods.load=org.dimdev.vanillafix.VanillaFixCoreMod",
"-Dmixin.env.compatLevel=JAVA_8",
//"-Dmixin.debug.verbose=true",
//"-Dmixin.debug.export=true",
"-Dmixin.debug=true",
"-Dmixin.debug.verbose=true",
"-Dmixin.debug.export=true",
"-Dmixin.checks.interfaces=true"
]
}

View file

@ -393,8 +393,6 @@ public class Schematic {
long setTime = 0;
long relightTime = 0;
// CubicChunks makes cubic worlds implement ICubicWorld
// Just "world instanceof ICubicWorld" would throw a class not found error
//noinspection InstanceofIncompatibleInterface
if (cubicChunks && ((ICubicWorld) world).isCubicWorld()) {
DimDoors.log.info("Setting cube blockstates");
ICubicWorld cubicWorld = (ICubicWorld) world;

View file

@ -1,6 +1,5 @@
package org.dimdev.dimdoors.shared.blocks;
import net.minecraft.block.Block;
import net.minecraft.block.BlockDoor;
import net.minecraft.block.BlockTrapDoor;
import net.minecraft.block.ITileEntityProvider;

View file

@ -1,10 +1,11 @@
package org.dimdev.dimdoors.shared.items;
import net.minecraft.item.ItemRecord;
import net.minecraft.util.SoundEvent;
import org.dimdev.dimdoors.DimDoors;
public class ItemRecord extends net.minecraft.item.ItemRecord {
protected ItemRecord(String recordName, SoundEvent soundIn) {
public class ItemModRecord extends ItemRecord {
protected ItemModRecord(String recordName, SoundEvent soundIn) {
super(recordName, soundIn);
setRegistryName(DimDoors.MODID, "record_" + recordName);
setUnlocalizedName("record");

View file

@ -5,7 +5,6 @@ import net.minecraft.util.text.TextComponentTranslation;
import org.dimdev.dimdoors.DimDoors;
import org.dimdev.dimdoors.client.TileEntityFloatingRiftRenderer;
import org.dimdev.dimdoors.shared.ModConfig;
import org.dimdev.dimdoors.shared.tileentities.TileEntityFloatingRift;
import org.dimdev.ddutils.Location;
import org.dimdev.ddutils.TeleportUtils;
import net.minecraft.client.util.ITooltipFlag;

View file

@ -53,7 +53,7 @@ public final class ModItems {
public static final ItemDimensionalTrapdoorWood WOOD_DIMENSIONAL_TRAPDOOR = new ItemDimensionalTrapdoorWood();
// Records
public static final ItemRecord CREEPY_RECORD = new ItemRecord("creepy", CREEPY);
public static final ItemModRecord CREEPY_RECORD = new ItemModRecord("creepy", CREEPY);
@SubscribeEvent
public static void registerItems(RegistryEvent.Register<Item> event) {

View file

@ -22,7 +22,6 @@ import net.minecraft.entity.Entity;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.EnumFacing;
import org.dimdev.dimdoors.shared.blocks.BlockDimensionalDoor;
import org.dimdev.dimdoors.shared.blocks.ModBlocks;
import java.util.Random;

View file

@ -6,24 +6,27 @@ import net.minecraft.init.MobEffects;
import net.minecraft.network.NetHandlerPlayServer;
import net.minecraft.network.PacketThreadUtil;
import net.minecraft.network.play.INetHandlerPlayServer;
import net.minecraft.network.play.client.CPacketConfirmTeleport;
import net.minecraft.network.play.client.CPacketPlayer;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.world.GameType;
import net.minecraft.world.WorldServer;
import org.apache.logging.log4j.Logger;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
@SuppressWarnings("unused") // Shadow
@SuppressWarnings({"unused", "NonConstantFieldWithUpperCaseName"}) // Shadow
@Mixin(NetHandlerPlayServer.class)
public abstract class MixinNetHandlerPlayServer implements INetHandlerPlayServer {
@Shadow public EntityPlayerMP player;
@Shadow private /*final*/ MinecraftServer server;
@Shadow @Final private MinecraftServer server;
@Shadow private int networkTickCount;
@Shadow private double firstGoodX;
@Shadow private double firstGoodY;
@ -34,9 +37,10 @@ public abstract class MixinNetHandlerPlayServer implements INetHandlerPlayServer
@Shadow private int lastPositionUpdate;
@Shadow private boolean floating;
@Shadow private Vec3d targetPos;
@Shadow private static final Logger LOGGER = null;
@Shadow @Final private static Logger LOGGER;
@Shadow private int movePacketCounter;
@Shadow private int lastMovePacketCounter;
@Shadow private int teleportId;
@Shadow public void disconnect(final ITextComponent textComponent) {}
@ -46,6 +50,34 @@ public abstract class MixinNetHandlerPlayServer implements INetHandlerPlayServer
@Shadow public void setPlayerLocation(double x, double y, double z, float yaw, float pitch) {}
/**
* Rewrite buggy vanilla method:
* <p>
* 1. After a teleport, the player should be teleported to the correct location even if they don't
* send a confirmation packet. This fixes MC-98153, preventing players from abusing it to teleport
* to the nether without changing coordinates by disconnecting before sending that packet, or to remain
* invulnerable (but unable to move) by not sending the confirmation packet. Interdimensional teleportation
* also seems much faster now.
* <p>
* 2. Block collisions should not be called until the move has been confirmed to be correct (the player
* didn't cheat). This prevents players from cheating to trigger block collision methods such as end
* portals. Also fixes MC-123364, where if anti-cheat is triggered when walking into an end portal,
* the position gets reverted but not the world, likely dropping the player into the void.
* <p>
* Bugs fixed:
* - https://bugs.mojang.com/browse/MC-89928
* - https://bugs.mojang.com/browse/MC-98153
* - https://bugs.mojang.com/browse/MC-123364
* - Movements made by server are reverted every second until client confirms teleport
* - Movement stats get increased with illegal moves
* - If teleport packet is received with ping > 1s, game is unplayable until player stops moving
* - lastGoodX/Y/Z not captured after teleport, resulting in "moved too quickly" spam after the client
* accepts a teleport in the same tick.
* <p>
* Improvements:
* - Correct position rather than cause jump-backs when player moves wrongly/into a block
*/
@Overwrite
@Override
public void processPlayer(CPacketPlayer packet) {
@ -53,120 +85,210 @@ public abstract class MixinNetHandlerPlayServer implements INetHandlerPlayServer
if (isMovePlayerPacketInvalid(packet)) {
disconnect(new TextComponentTranslation("multiplayer.disconnect.invalid_player_movement"));
} else {
WorldServer world = server.getWorld(player.dimension);
return;
}
if (player.queuedEndExit) return;
// Optimization: Check for queuedEndExit first, get or load the world if necessary only.
if (player.queuedEndExit) return;
if (networkTickCount == 0) {
captureCurrentPosition();
}
WorldServer world = server.getWorld(player.dimension);
if (targetPos != null) {
if (networkTickCount - lastPositionUpdate > 20) {
lastPositionUpdate = networkTickCount;
setPlayerLocation(targetPos.x, targetPos.y, targetPos.z, player.rotationYaw, player.rotationPitch);
}
} else {
if (networkTickCount == 0) {
captureCurrentPosition();
}
if (targetPos != null) {
// Fix: Vanilla did this, but why? It's impossible for a packet to get lost with TCP. If we send
// another update packet after just one second, a one-second ping will make the game completely
// unplayable after the server updates the player's position, since by the time the client confirms
// a position change, the server will have sent it another one, repeatedly causing the player to jump
// back until they stop moving for whatever the ping time is.
/*if (networkTickCount - lastPositionUpdate > 20) {
lastPositionUpdate = networkTickCount;
if (player.isRiding()) {
player.setPositionAndRotation(player.posX, player.posY, player.posZ, packet.getYaw(player.rotationYaw), packet.getPitch(player.rotationPitch));
server.getPlayerList().serverUpdateMovingPlayer(player);
} else {
double oldX = player.posX;
double oldY = player.posY;
double oldZ = player.posZ;
double oldY2 = player.posY;
// Fix: Vanilla code was: setPlayerLocation(targetPos.x, targetPos.y, targetPos.z,
// player.rotationYaw, player.rotationPitch); Why would the server revert movements
// after a teleport just because the client didn't confirm it? I'm assuming that they
// intended to send another updated move packet:
setPlayerLocation(player.posX, player.posY, player.posZ, player.rotationYaw, player.rotationPitch);
}*/
double packetX = packet.getX(player.posX);
double packetY = packet.getY(player.posY);
double packetZ = packet.getZ(player.posZ);
float packetYaw = packet.getYaw(player.rotationYaw);
float packetPitch = packet.getPitch(player.rotationPitch);
// Instead, we will send another packet both here and in processConfirmTeleport if the position the client
// was sent is no longer good (exceeds tolerance):
double xDiff = packetX - firstGoodX;
double yDiff = packetY - firstGoodY;
double zDiff = packetZ - firstGoodZ;
double speedSq = player.motionX * player.motionX + player.motionY * player.motionY + player.motionZ * player.motionZ;
double distanceSq = xDiff * xDiff + yDiff * yDiff + zDiff * zDiff;
LOGGER.info("Player sent move packets before confirming");
if (targetPos.squareDistanceTo(player.posX, player.posY, player.posZ) > 1.0D) {
LOGGER.info("Distance became too big: " + targetPos + " vs. " + new Vec3d(player.posX, player.posY, player.posY));
setPlayerLocation(player.posX, player.posY, player.posZ, player.rotationYaw, player.rotationPitch);
}
if (player.isPlayerSleeping()) {
if (distanceSq > 1.0D) {
setPlayerLocation(player.posX, player.posY, player.posZ, packet.getYaw(player.rotationYaw), packet.getPitch(player.rotationPitch));
}
} else {
++movePacketCounter;
int packetCount = movePacketCounter - lastMovePacketCounter;
// Ignore the packet, the client thinks it's still at the old position
return;
}
if (packetCount > 5) {
LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", player.getName(), packetCount);
packetCount = 1;
}
lastPositionUpdate = networkTickCount;
if (!player.isInvulnerableDimensionChange() && (!player.getServerWorld().getGameRules().getBoolean("disableElytraMovementCheck") || !player.isElytraFlying())) {
float maxDistancePerTic = player.isElytraFlying() ? 300.0F : 100.0F;
if (player.isRiding()) {
player.setPositionAndRotation(player.posX, player.posY, player.posZ, packet.getYaw(player.rotationYaw), packet.getPitch(player.rotationPitch));
server.getPlayerList().serverUpdateMovingPlayer(player);
} else {
double prevX = player.posX;
double prevY = player.posY;
double prevZ = player.posZ;
if (distanceSq - speedSq > maxDistancePerTic * packetCount && (!server.isSinglePlayer() || !server.getServerOwner().equals(player.getName()))) {
LOGGER.warn("{} moved too quickly! {},{},{}", player.getName(), xDiff, yDiff, zDiff);
setPlayerLocation(player.posX, player.posY, player.posZ, player.rotationYaw, player.rotationPitch);
return;
}
}
double packetX = packet.getX(player.posX);
double packetY = packet.getY(player.posY);
double packetZ = packet.getZ(player.posZ);
float packetYaw = packet.getYaw(player.rotationYaw);
float packetPitch = packet.getPitch(player.rotationPitch);
boolean notInsideBlock = world.getCollisionBoxes(player, player.getEntityBoundingBox().shrink(0.0625D)).isEmpty();
xDiff = packetX - lastGoodX;
yDiff = packetY - lastGoodY;
zDiff = packetZ - lastGoodZ;
// Calculate difference from last tick
double xDiff = packetX - firstGoodX;
double yDiff = packetY - firstGoodY;
double zDiff = packetZ - firstGoodZ;
double distanceSq = xDiff * xDiff + yDiff * yDiff + zDiff * zDiff;
if (player.onGround && !packet.isOnGround() && yDiff > 0.0D) {
player.jump();
}
double motionSq = player.motionX * player.motionX + player.motionY * player.motionY + player.motionZ * player.motionZ;
player.move(MoverType.PLAYER, xDiff, yDiff, zDiff);
player.onGround = packet.isOnGround();
double oldYDiff = yDiff;
if (player.isPlayerSleeping()) {
if (distanceSq > 1.0D) {
// The player tried to move while sleeping. Send the player the correct position.
setPlayerLocation(player.posX, player.posY, player.posZ, packet.getYaw(player.rotationYaw), packet.getPitch(player.rotationPitch));
}
} else {
movePacketCounter++;
int packetCount = movePacketCounter - lastMovePacketCounter;
xDiff = packetX - player.posX;
yDiff = packetY - player.posY;
if (yDiff > -0.5D || yDiff < 0.5D) { // TODO: But why?
yDiff = 0.0D;
}
zDiff = packetZ - player.posZ;
distanceSq = xDiff * xDiff + yDiff * yDiff + zDiff * zDiff;
if (packetCount > 5) {
LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", player.getName(), packetCount);
packetCount = 1;
}
boolean movedWrongly = false;
if (!player.isInvulnerableDimensionChange() && distanceSq > 0.0625D && !player.isPlayerSleeping() && !player.interactionManager.isCreative() && player.interactionManager.getGameType() != GameType.SPECTATOR) {
movedWrongly = true;
LOGGER.warn("{} moved wrongly!", player.getName());
}
// Fix: Disable isInvulnerableDimensionChange, not necessary if we don't revert position after teleport anymore
if (/*!player.isInvulnerableDimensionChange() && */!(player.getServerWorld().getGameRules().getBoolean("disableElytraMovementCheck") && player.isElytraFlying())) {
// Not really anti-cheat. Max speed would be 447 blocks/second with 5 packets/tick... Also, shouldn't
// distance increase linearly with the packet count rather than proportional to sqrt(packetCount)?
float maxDistanceSqPerPacket = player.isElytraFlying() ? 300.0F : 100.0F;
// Fix https://bugs.mojang.com/browse/MC-98153
//player.setPositionAndRotation(packetX, packetY, packetZ, packetYaw, packetPitch);
//player.addMovementStat(player.posX - oldX, player.posY - oldY, player.posZ - oldZ);
player.addMovementStat(packetX - oldX, packetY - oldY, packetZ - oldZ);
// Fix https://bugs.mojang.com/browse/MC-123364 (partially, players can still cheat to teleport into a portal)
if (!player.isInvulnerableDimensionChange() && !player.noClip && !player.isPlayerSleeping()) {
boolean oldPositionEmpty = world.getCollisionBoxes(player, player.getEntityBoundingBox().shrink(0.0625D)).isEmpty();
if (notInsideBlock && (movedWrongly || !oldPositionEmpty)) {
setPlayerLocation(oldX, oldY, oldZ, packetYaw, packetPitch);
return;
}
}
floating = oldYDiff >= -0.03125D;
floating &= !server.isFlightAllowed() && !player.capabilities.allowFlying;
floating &= !player.isPotionActive(MobEffects.LEVITATION) && !player.isElytraFlying() && !world.checkBlockCollision(player.getEntityBoundingBox().grow(0.0625D).expand(0.0D, -0.55D, 0.0D));
player.onGround = packet.isOnGround();
server.getPlayerList().serverUpdateMovingPlayer(player);
player.handleFalling(player.posY - oldY2, packet.isOnGround());
lastGoodX = player.posX;
lastGoodY = player.posY;
lastGoodZ = player.posZ;
if (distanceSq - motionSq > maxDistanceSqPerPacket * packetCount/* && !(server.isSinglePlayer() && server.getServerOwner().equals(player.getName()))*/) { // TODO: Single-player server owner check disabled for testing
LOGGER.warn("{} moved too quickly! {},{},{}", player.getName(), xDiff, yDiff, zDiff);
setPlayerLocation(player.posX, player.posY, player.posZ, player.rotationYaw, player.rotationPitch);
return;
}
}
boolean wasInsideBlock = !world.getCollisionBoxes(player, player.getEntityBoundingBox().shrink(0.0625D)).isEmpty();
// Calculate difference from last packet
xDiff = packetX - lastGoodX;
yDiff = packetY - lastGoodY;
zDiff = packetZ - lastGoodZ;
if (player.onGround && !packet.isOnGround() && yDiff > 0.0D) {
player.jump();
}
// Move the player towards the desired position, but not passing through solid
// blocks, past block edges while sneaking, or climbing stairs that are too tall.
// TODO: Players could cheat to avoid collisions with non-solid blocks (end portal), fix this.
player.move(MoverType.PLAYER, xDiff, yDiff, zDiff);
player.onGround = packet.isOnGround();
// Fix: If a collision teleported the player, just sync the client without checking that the move was legal.
// Entity.move already made sure that the player didn't cheat, and reverting the move would be wrong because
// the prevX/Y/Z is no longer good in the new dimension. This fixes MC-123364.
if (player.isInvulnerableDimensionChange()) {
LOGGER.info("Doing teleport");
// A better name for invulnerableDimensionChange would be "lastBlockCollisionCausedPlayerMove". See
// https://github.com/ModCoderPack/MCPBot-Issues/issues/624. This happens when the move caused a
// collision that teleported the player elsewhere.
// Fix: Immediately set the correct position after a teleport and, rather than reverting it and waiting
// for the player to confirm the teleport (which they might never do). This fixes MC-98153.
setPlayerLocation(player.posX, player.posY, player.posZ, player.rotationYaw, player.rotationPitch);
// Fix: Disable invulnerability after teleport, this could be abused by players, allowing them to stay
// invulnerable but unable to move after a teleport.
player.clearInvulnerableDimensionChange();
// Fix: In processConfirmTeleport (which we moved here, since we're doing the move immediately), only
// lastGoodX/Y/Z are set to the current position, but firstGoodX/Y/Z should be updated too (this can
// be done using captureCurrentPosition). Otherwise, this would result in "moved wrongly" messages if
// the client both accepts the teleport and starts sending move packets that are correct within the same tick.
captureCurrentPosition();
yDiff = 0; // Reset fall distance
} else {
// Calculate difference from position accepted by Entity.move
xDiff = packetX - player.posX;
yDiff = packetY - player.posY;
zDiff = packetZ - player.posZ;
double yDiffA = yDiff > -0.5D || yDiff < 0.5D ? 0.0D : yDiff;
distanceSq = xDiff * xDiff + yDiffA * yDiffA + zDiff * zDiff;
boolean movedWrongly = false;
// Optimization: Remove !player.isPlayerSleeping() check, that was already checked
if (distanceSq > 0.0625D /*&& !player.isPlayerSleeping()*/ && !player.interactionManager.isCreative() && player.interactionManager.getGameType() != GameType.SPECTATOR) {
movedWrongly = true;
LOGGER.warn("{} moved wrongly!", player.getName());
}
boolean wouldMoveInsideBlock = !world.getCollisionBoxes(player, player.getEntityBoundingBox().offset(xDiff, yDiff, zDiff).shrink(0.0625D)).isEmpty();
if (!movedWrongly && !wouldMoveInsideBlock || wasInsideBlock || player.noClip) {
// If the position the player wanted is close enough to what Entity.move calculated
// and not inside a block, or the player was noclipping (either because player.noClip
// was true or they were inside a block), use the client's position instead
player.setPositionAndRotation(packetX, packetY, packetZ, packetYaw, packetPitch);
} else {
// Improvement: Instead of reverting the move to prevX/Y/Z, accept the corrected move, since the
// Entity.move method made sure that it was legal. This has the advantage of just correcting the
// position rather than causing jump-backs and is safer against in case a block collision forgets
// to set invulnerableDimensionChange after a teleport (only causes a log message and some extra
// unnecessary checks).
setPlayerLocation(player.posX, player.posY, player.posZ, packetYaw, packetPitch);
player.addMovementStat(player.posX - prevX, player.posY - prevY, player.posZ - prevZ);
return;
}
// Fix: Update movement stats to the corrected position rather than the position the client wanted.
// This prevents illegal, cancelled (vanilla) or corrected (with improvement above), moves from updating
// stats.
player.addMovementStat(player.posX - prevX, player.posY - prevY, player.posZ - prevZ);
}
floating = yDiff >= -0.03125D;
floating &= !server.isFlightAllowed() && !player.capabilities.allowFlying;
floating &= !player.isPotionActive(MobEffects.LEVITATION) && !player.isElytraFlying() && !world.checkBlockCollision(player.getEntityBoundingBox().grow(0.0625D).expand(0.0D, -0.55D, 0.0D));
player.onGround = packet.isOnGround();
server.getPlayerList().serverUpdateMovingPlayer(player);
player.handleFalling(player.posY - prevY, packet.isOnGround());
lastGoodX = player.posX;
lastGoodY = player.posY;
lastGoodZ = player.posZ;
}
}
}
/**
* Update this method for changes made above.
* <p>
* Bugs fixed:
* - Movements made by server are reverted when client confirms teleport
*/
@Overwrite
@Override
public void processConfirmTeleport(CPacketConfirmTeleport packet) {
PacketThreadUtil.checkThreadAndEnqueue(packet, this, player.getServerWorld());
if (packet.getTeleportId() == teleportId) {
if (targetPos.squareDistanceTo(player.posX, player.posY, player.posZ) > 1.0D) {
LOGGER.info("Position accepted no longer good");
// The client accepted the position change, but it's too late, something moved the player. Sync it again.
setPlayerLocation(player.posX, player.posY, player.posZ, player.rotationYaw, player.rotationPitch);
} else {
LOGGER.info("Teleport accepted");
targetPos = null;
}
}
}

View file

@ -143,7 +143,6 @@ dimdoors.general.depthSpreadFactor=Depth Spread Factor
dimdoors.general.depthSpreadFactor.tooltip=The scale of the dispersion when escaping from a pocket or limbo, in blocks/depth. It is important to remember that by default, limbo is treated as depth 50.
dimdoors.general.riftCloseSpeed=Rift Close Speed
dimdoors.general.riftCloseSpeed.tooltip=The speed at which rifts close when using the rift remover, in units of rift size per tick.
dimdoors.general.closeDoorBehind.tooltip=When true, Dimensional Doors will automatically close when the player enters their portal.
dimdoors.general.teleportOffset=Teleport Offset
dimdoors.general.teleportOffset.tooltip=Distance in blocks to teleport the player in front of the dimensional door.
dimdoors.general.riftBoundingBoxInCreative=Rift Bounding Box in Creative
@ -201,4 +200,4 @@ dimdoors.graphics.showRiftCore.tooltip=Set this to true to always show rifts' co
dimdoors.graphics.riftSize=Rift Size
dimdoors.graphics.riftSize.tooltip=Multiplier affecting how large rifts should be rendered, 1 being the default size.
dimdoors.graphics.riftJitter=Rift Jitter
dimdoors.graphics.riftSize.tooltip=Multiplier affecting how much rifts should jitter, 1 being the default size.
dimdoors.graphics.riftJitter.tooltip=Multiplier affecting how much rifts should jitter, 1 being the default size.

View file

@ -109,8 +109,8 @@ dimdoors.general=Options Généralles
dimdoors.general.tooltip=Options de configuration généralles pour le mod
dimdoors.general.baseDimensionID=Numéro de dimension de base
dimdoors.general.baseDimensionID.tooltip=Le numéro de la première dimension de Portes dimensionelles. Les autres dimensions utiliseront des numéros consécutifs. Il est fortement recommendé de laisser cette option à sa valeur par défaut.
dimdoors.general.useStatusBar.name=Messages dans la barre d'état
dimdoors.general.useStatusBar.comment=Utiliser la barre d'état pour envoyer des messages plutôt que la boîte de discussion.
dimdoors.general.useStatusBar=Messages dans la barre d'état
dimdoors.general.useStatusBar.tooltip=Utiliser la barre d'état pour envoyer des messages plutôt que la boîte de discussion.
dimdoors.pockets=Options sur les dimensions de poche
dimdoors.pockets.tooltip=Options qui déterminent l'espacement et la grandeur maximalle des dimensions de poche dans la dimension qui les contient
dimdoors.world=Options de génération de monde