// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.core.prefab.selection.standard;

import java.io.IOException;
import com.hypixel.hytale.server.core.modules.entity.EntityModule;
import it.unimi.dsi.fastutil.longs.LongIterator;
import com.hypixel.hytale.server.core.asset.type.blockhitbox.BlockBoundingBoxes;
import it.unimi.dsi.fastutil.longs.LongCollection;
import com.hypixel.hytale.protocol.packets.interface_.EditorSelection;
import com.hypixel.hytale.protocol.packets.interface_.FluidChange;
import com.hypixel.hytale.protocol.packets.interface_.BlockChange;
import com.hypixel.hytale.metrics.MetricResults;
import java.util.Map;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.VariantRotation;
import com.hypixel.hytale.server.core.util.FillerBlockUtil;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.math.matrix.Matrix4d;
import com.hypixel.hytale.math.vector.Vector3f;
import com.hypixel.hytale.server.core.modules.entity.component.HeadRotation;
import com.hypixel.hytale.server.core.universe.world.chunk.BlockRotationUtil;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.Rotation;
import com.hypixel.hytale.math.Axis;
import com.hypixel.hytale.server.core.modules.entity.component.FromPrefab;
import com.hypixel.hytale.server.core.prefab.event.PrefabPlaceEntityEvent;
import java.util.Iterator;
import java.util.logging.Level;
import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection;
import com.hypixel.hytale.assetstore.map.IndexedLookupTableAssetMap;
import com.hypixel.hytale.server.core.asset.type.fluid.Fluid;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.universe.world.chunk.section.FluidSection;
import com.hypixel.hytale.server.core.universe.world.chunk.ChunkColumn;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk;
import com.hypixel.hytale.protocol.Opacity;
import com.hypixel.hytale.server.core.asset.type.fluid.FluidTicker;
import com.hypixel.hytale.server.core.universe.world.accessor.ChunkAccessor;
import it.unimi.dsi.fastutil.longs.LongSet;
import java.util.function.IntUnaryOperator;
import com.hypixel.hytale.assetstore.map.BlockTypeAssetMap;
import java.util.concurrent.atomic.AtomicInteger;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import com.hypixel.hytale.server.core.prefab.selection.mask.BlockMask;
import com.hypixel.hytale.server.core.command.system.CommandSender;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import java.util.BitSet;
import com.hypixel.hytale.component.SystemType;
import com.hypixel.hytale.component.ComponentRegistry;
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.StateData;
import com.hypixel.hytale.component.RemoveReason;
import com.hypixel.hytale.component.AddReason;
import com.hypixel.hytale.server.core.modules.block.BlockModule;
import org.bson.BsonValue;
import org.bson.BsonDocument;
import com.hypixel.hytale.server.core.universe.world.meta.BlockState;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import com.hypixel.hytale.server.core.blocktype.component.BlockPhysics;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import com.hypixel.hytale.math.block.BlockUtil;
import javax.annotation.Nullable;
import it.unimi.dsi.fastutil.ints.IntList;
import com.hypixel.hytale.server.core.universe.world.World;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.hypixel.hytale.component.Holder;
import java.util.List;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import javax.annotation.Nonnull;
import com.hypixel.hytale.math.vector.Vector3i;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.metrics.MetricsRegistry;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.Ref;
import java.util.function.Consumer;
import com.hypixel.hytale.metrics.MetricProvider;
import com.hypixel.hytale.protocol.packets.interface_.EditorBlocksChange;
import com.hypixel.hytale.server.core.io.NetworkSerializable;

public class BlockSelection implements NetworkSerializable<EditorBlocksChange>, MetricProvider
{
    public static final Consumer<Ref<EntityStore>> DEFAULT_ENTITY_CONSUMER;
    public static final MetricsRegistry<BlockSelection> METRICS_REGISTRY;
    private static final HytaleLogger LOGGER;
    private int x;
    private int y;
    private int z;
    private int anchorX;
    private int anchorY;
    private int anchorZ;
    private int prefabId;
    @Nonnull
    private Vector3i min;
    @Nonnull
    private Vector3i max;
    @Nonnull
    private final Long2ObjectMap<BlockHolder> blocks;
    @Nonnull
    private final Long2ObjectMap<FluidHolder> fluids;
    @Nonnull
    private final List<Holder<EntityStore>> entities;
    private final ReentrantReadWriteLock blocksLock;
    private final ReentrantReadWriteLock entitiesLock;
    static final /* synthetic */ boolean $assertionsDisabled;
    
    public BlockSelection() {
        this.prefabId = -1;
        this.min = Vector3i.ZERO;
        this.max = Vector3i.ZERO;
        this.blocksLock = new ReentrantReadWriteLock();
        this.entitiesLock = new ReentrantReadWriteLock();
        this.blocks = new Long2ObjectOpenHashMap<BlockHolder>();
        this.fluids = new Long2ObjectOpenHashMap<FluidHolder>();
        this.entities = new ObjectArrayList<Holder<EntityStore>>();
    }
    
    public BlockSelection(final int initialBlockCapacity, final int initialEntityCapacity) {
        this.prefabId = -1;
        this.min = Vector3i.ZERO;
        this.max = Vector3i.ZERO;
        this.blocksLock = new ReentrantReadWriteLock();
        this.entitiesLock = new ReentrantReadWriteLock();
        this.blocks = new Long2ObjectOpenHashMap<BlockHolder>(initialBlockCapacity);
        this.fluids = new Long2ObjectOpenHashMap<FluidHolder>(initialBlockCapacity);
        this.entities = new ObjectArrayList<Holder<EntityStore>>(initialEntityCapacity);
    }
    
    public BlockSelection(@Nonnull final BlockSelection other) {
        this.prefabId = -1;
        this.min = Vector3i.ZERO;
        this.max = Vector3i.ZERO;
        this.blocksLock = new ReentrantReadWriteLock();
        this.entitiesLock = new ReentrantReadWriteLock();
        if (other == this) {
            throw new IllegalArgumentException("Cannot duplicate a BlockSelection with this method! Use clone()!");
        }
        this.blocks = new Long2ObjectOpenHashMap<BlockHolder>(other.getBlockCount());
        this.fluids = new Long2ObjectOpenHashMap<FluidHolder>(other.getFluidCount());
        this.entities = new ObjectArrayList<Holder<EntityStore>>(other.getEntityCount());
        this.copyPropertiesFrom(other);
        this.add(other);
    }
    
    public int getX() {
        return this.x;
    }
    
    public int getY() {
        return this.y;
    }
    
    public int getZ() {
        return this.z;
    }
    
    public int getAnchorX() {
        return this.anchorX;
    }
    
    public int getAnchorY() {
        return this.anchorY;
    }
    
    public int getAnchorZ() {
        return this.anchorZ;
    }
    
    @Nonnull
    public Vector3i getSelectionMin() {
        return this.min.clone();
    }
    
    @Nonnull
    public Vector3i getSelectionMax() {
        return this.max.clone();
    }
    
    public boolean hasSelectionBounds() {
        return !this.min.equals(Vector3i.ZERO) || !this.max.equals(Vector3i.ZERO);
    }
    
    public int getBlockCount() {
        this.blocksLock.readLock().lock();
        try {
            return this.blocks.size();
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
    }
    
    public int getFluidCount() {
        this.blocksLock.readLock().lock();
        try {
            return this.fluids.size();
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
    }
    
    public int getSelectionVolume() {
        final int xLength = this.max.x - this.min.x;
        final int yLength = this.max.y - this.min.y;
        final int zLength = this.max.z - this.min.z;
        return xLength * yLength & zLength;
    }
    
    public int getEntityCount() {
        this.entitiesLock.readLock().lock();
        try {
            return this.entities.size();
        }
        finally {
            this.entitiesLock.readLock().unlock();
        }
    }
    
    public void setPosition(final int x, final int y, final int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
    
    public void setAnchorAtWorldPos(final int anchorX, final int anchorY, final int anchorZ) {
        this.setAnchor(anchorX - this.x, anchorY - this.y, anchorZ - this.z);
    }
    
    public void setAnchor(final int anchorX, final int anchorY, final int anchorZ) {
        this.anchorX = anchorX;
        this.anchorY = anchorY;
        this.anchorZ = anchorZ;
    }
    
    public void setSelectionArea(@Nonnull final Vector3i min, @Nonnull final Vector3i max) {
        this.min = Vector3i.min(min, max);
        this.max = Vector3i.max(min, max);
    }
    
    public void setPrefabId(final int id) {
        this.prefabId = id;
    }
    
    public void copyPropertiesFrom(@Nonnull final BlockSelection other) {
        this.x = other.x;
        this.y = other.y;
        this.z = other.z;
        this.anchorX = other.anchorX;
        this.anchorY = other.anchorY;
        this.anchorZ = other.anchorZ;
        this.min = other.min.clone();
        this.max = other.max.clone();
    }
    
    public boolean canPlace(@Nonnull final World world, @Nonnull final Vector3i position, @Nullable final IntList mask) {
        return this.compare((x1, y1, z1, block) -> {
            final int blockX = x1 + position.getX() - this.anchorX;
            final int blockY = y1 + position.getY() - this.anchorY;
            final int blockZ = z1 + position.getZ() - this.anchorZ;
            final int blockId = world.getBlock(blockX, blockY, blockZ);
            return blockId == 0 || mask == null || mask.contains(blockId);
        });
    }
    
    public boolean matches(@Nonnull final World world, @Nonnull final Vector3i position) {
        return this.compare((x1, y1, z1, block) -> {
            final int blockX = x1 + position.getX() - this.anchorX;
            final int blockY = y1 + position.getY() - this.anchorY;
            final int blockZ = z1 + position.getZ() - this.anchorZ;
            final int blockId = world.getBlock(blockX, blockY, blockZ);
            return block.blockId == blockId;
        });
    }
    
    public boolean compare(@Nonnull final BlockComparingIterator iterator) {
        for (final Long2ObjectMap.Entry<BlockHolder> entry : this.blocks.long2ObjectEntrySet()) {
            final long packed = entry.getLongKey();
            final BlockHolder value = entry.getValue();
            final int x1 = BlockUtil.unpackX(packed);
            final int y1 = BlockUtil.unpackY(packed);
            final int z1 = BlockUtil.unpackZ(packed);
            if (!iterator.test(x1, y1, z1, value)) {
                return false;
            }
        }
        return true;
    }
    
    public boolean hasBlockAtWorldPos(final int x, final int y, final int z) {
        return this.hasBlockAtLocalPos(x - this.x, y - this.y, z - this.z);
    }
    
    public boolean hasBlockAtLocalPos(final int x, final int y, final int z) {
        this.blocksLock.readLock().lock();
        try {
            return this.blocks.containsKey(BlockUtil.pack(x, y, z));
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
    }
    
    public int getBlockAtWorldPos(final int x, final int y, final int z) {
        return this.getBlockAtLocalPos(x - this.x, y - this.y, z - this.z);
    }
    
    private int getBlockAtLocalPos(final int x, final int y, final int z) {
        this.blocksLock.readLock().lock();
        try {
            final BlockHolder blockHolder = this.blocks.get(BlockUtil.pack(x, y, z));
            if (blockHolder == null) {
                return Integer.MIN_VALUE;
            }
            return blockHolder.blockId();
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
    }
    
    public BlockHolder getBlockHolderAtWorldPos(final int x, final int y, final int z) {
        return this.getBlockHolderAtLocalPos(x - this.x, y - this.y, z - this.z);
    }
    
    private BlockHolder getBlockHolderAtLocalPos(final int x, final int y, final int z) {
        this.blocksLock.readLock().lock();
        try {
            return this.blocks.get(BlockUtil.pack(x, y, z));
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
    }
    
    public int getFluidAtWorldPos(final int x, final int y, final int z) {
        return this.getFluidAtLocalPos(x - this.x, y - this.y, z - this.z);
    }
    
    private int getFluidAtLocalPos(final int x, final int y, final int z) {
        this.blocksLock.readLock().lock();
        try {
            final FluidHolder fluidStore = this.fluids.get(BlockUtil.pack(x, y, z));
            if (fluidStore == null) {
                return Integer.MIN_VALUE;
            }
            return fluidStore.fluidId();
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
    }
    
    public byte getFluidLevelAtWorldPos(final int x, final int y, final int z) {
        return this.getFluidLevelAtLocalPos(x - this.x, y - this.y, z - this.z);
    }
    
    private byte getFluidLevelAtLocalPos(final int x, final int y, final int z) {
        this.blocksLock.readLock().lock();
        try {
            final FluidHolder fluidStore = this.fluids.get(BlockUtil.pack(x, y, z));
            if (fluidStore == null) {
                return 0;
            }
            return fluidStore.fluidLevel();
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
    }
    
    public int getSupportValueAtWorldPos(final int x, final int y, final int z) {
        return this.getSupportValueAtLocalPos(x - this.x, y - this.y, z - this.z);
    }
    
    private int getSupportValueAtLocalPos(final int x, final int y, final int z) {
        this.blocksLock.readLock().lock();
        try {
            final BlockHolder blockHolder = this.blocks.get(BlockUtil.pack(x, y, z));
            if (blockHolder == null) {
                return 0;
            }
            return blockHolder.supportValue();
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
    }
    
    @Nullable
    public Holder<ChunkStore> getStateAtWorldPos(final int x, final int y, final int z) {
        return this.getStateAtLocalPos(x - this.x, y - this.y, z - this.z);
    }
    
    @Nullable
    private Holder<ChunkStore> getStateAtLocalPos(final int x, final int y, final int z) {
        this.blocksLock.readLock().lock();
        try {
            final BlockHolder blockHolder = this.blocks.get(BlockUtil.pack(x, y, z));
            if (blockHolder == null) {
                return null;
            }
            final Holder<ChunkStore> holder = blockHolder.holder();
            return (holder != null) ? holder.clone() : null;
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
    }
    
    public void forEachBlock(@Nonnull final BlockIterator iterator) {
        this.blocksLock.readLock().lock();
        try {
            Long2ObjectMaps.fastForEach(this.blocks, e -> {
                final long packed = e.getLongKey();
                final BlockHolder block = (BlockHolder)e.getValue();
                final int x1 = BlockUtil.unpackX(packed);
                final int y1 = BlockUtil.unpackY(packed);
                final int z1 = BlockUtil.unpackZ(packed);
                iterator.accept(x1, y1, z1, block);
            });
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
    }
    
    public void forEachFluid(@Nonnull final FluidIterator iterator) {
        this.blocksLock.readLock().lock();
        try {
            Long2ObjectMaps.fastForEach(this.fluids, e -> {
                final long packed = e.getLongKey();
                final FluidHolder block = (FluidHolder)e.getValue();
                final int x1 = BlockUtil.unpackX(packed);
                final int y1 = BlockUtil.unpackY(packed);
                final int z1 = BlockUtil.unpackZ(packed);
                iterator.accept(x1, y1, z1, block.fluidId(), block.fluidLevel());
            });
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
    }
    
    public void forEachEntity(final Consumer<Holder<EntityStore>> consumer) {
        this.entitiesLock.readLock().lock();
        try {
            this.entities.forEach(consumer);
        }
        finally {
            this.entitiesLock.readLock().unlock();
        }
    }
    
    public void copyFromAtWorld(final int x, final int y, final int z, @Nonnull final WorldChunk other, @Nullable final BlockPhysics blockPhysics) {
        this.addBlockAtWorldPos(x, y, z, other.getBlock(x, y, z), other.getRotationIndex(x, y, z), other.getFiller(x, y, z), (blockPhysics != null) ? blockPhysics.get(x, y, z) : 0, other.getBlockComponentHolder(x, y, z));
        this.addFluidAtWorldPos(x, y, z, other.getFluidId(x, y, z), other.getFluidLevel(x, y, z));
    }
    
    public void addEmptyAtWorldPos(final int x, final int y, final int z) {
        this.addBlockAtWorldPos(x, y, z, 0, 0, 0, 0);
        this.addFluidAtWorldPos(x, y, z, 0, (byte)0);
    }
    
    public void addBlockAtWorldPos(final int x, final int y, final int z, final int block, final int rotation, final int filler, final int supportValue) {
        this.addBlockAtWorldPos(x, y, z, block, rotation, filler, supportValue, null);
    }
    
    public void addBlockAtWorldPos(final int x, final int y, final int z, final int block, final int rotation, final int filler, final int supportValue, final Holder<ChunkStore> state) {
        this.addBlockAtLocalPos(x - this.x, y - this.y, z - this.z, block, rotation, filler, supportValue, state);
    }
    
    public void addBlockAtLocalPos(final int x, final int y, final int z, final int block, final int rotation, final int filler, final int supportValue) {
        this.addBlockAtLocalPos(x, y, z, block, rotation, filler, supportValue, null);
    }
    
    public void addBlockAtLocalPos(final int x, final int y, final int z, final int block, final int rotation, final int filler, final int supportValue, final Holder<ChunkStore> state) {
        this.blocksLock.writeLock().lock();
        try {
            this.addBlock0(x, y, z, block, rotation, filler, supportValue, state);
        }
        finally {
            this.blocksLock.writeLock().unlock();
        }
    }
    
    private void addBlock0(final int x, final int y, final int z, final int block, final int rotation, final int filler, final int supportValue, final Holder<ChunkStore> state) {
        this.blocks.put(BlockUtil.pack(x, y, z), new BlockHolder(block, rotation, filler, supportValue, state));
    }
    
    private void addBlock0(final int x, final int y, final int z, @Nonnull final BlockHolder block) {
        this.blocks.put(BlockUtil.pack(x, y, z), block.cloneBlockHolder());
    }
    
    public void addFluidAtWorldPos(final int x, final int y, final int z, final int fluidId, final byte fluidLevel) {
        this.addFluidAtLocalPos(x - this.x, y - this.y, z - this.z, fluidId, fluidLevel);
    }
    
    public void addFluidAtLocalPos(final int x, final int y, final int z, final int fluidId, final byte fluidLevel) {
        this.blocksLock.writeLock().lock();
        try {
            this.addFluid0(x, y, z, fluidId, fluidLevel);
        }
        finally {
            this.blocksLock.writeLock().unlock();
        }
    }
    
    private void addFluid0(final int x, final int y, final int z, final int fluidId, final byte fluidLevel) {
        this.fluids.put(BlockUtil.pack(x, y, z), new FluidHolder(fluidId, fluidLevel));
    }
    
    private void addEntity0(final Holder<EntityStore> holder) {
        this.entities.add(holder);
    }
    
    public void reserializeBlockStates(final ChunkStore store, final boolean destructive) {
        this.blocksLock.writeLock().lock();
        try {
            this.blocks.replaceAll((k, b) -> {
                Holder<ChunkStore> holder = b.holder();
                if (holder == null && b.filler == 0) {
                    final BlockType blockType = BlockType.getAssetMap().getAsset(b.blockId);
                    if (blockType == null) {
                        return b;
                    }
                    else {
                        if (blockType.getBlockEntity() != null) {
                            holder = blockType.getBlockEntity().clone();
                        }
                        final StateData state = blockType.getState();
                        if (state != null && state.getId() != null) {
                            final Vector3i position = new Vector3i(BlockUtil.unpackX(k), BlockUtil.unpackY(k), BlockUtil.unpackZ(k));
                            final Codec<? extends BlockState> codec = BlockState.CODEC.getCodecFor(state.getId());
                            if (codec == null) {
                                return b;
                            }
                            else {
                                final BlockState blockState = (BlockState)codec.decode(new BsonDocument());
                                if (blockState == null) {
                                    return b;
                                }
                                else {
                                    blockState.setPosition(null, position);
                                    holder = blockState.toHolder();
                                }
                            }
                        }
                    }
                }
                if (holder != null && b.filler != 0) {
                    return new BlockHolder(b.blockId(), b.rotation(), b.filler(), b.supportValue(), null);
                }
                else if (holder == null) {
                    return b;
                }
                else {
                    try {
                        final ComponentRegistry<ChunkStore> registry = ChunkStore.REGISTRY;
                        final ComponentRegistry.Data<ChunkStore> data = registry.getData();
                        final SystemType<ChunkStore, BlockModule.MigrationSystem> systemType = BlockModule.get().getMigrationSystemType();
                        final BitSet systemIndexes = data.getSystemIndexesForType(systemType);
                        int systemIndex = -1;
                        while (true) {
                            systemIndex = systemIndexes.nextSetBit(systemIndex + 1);
                            final Object o;
                            if (o >= 0) {
                                final BlockModule.MigrationSystem system = data.getSystem(systemIndex, systemType);
                                if (system.test(registry, holder.getArchetype())) {
                                    system.onEntityAdd(holder, AddReason.LOAD, store.getStore());
                                }
                                else {
                                    continue;
                                }
                            }
                            else {
                                break;
                            }
                        }
                        int systemIndex2 = -1;
                        while (true) {
                            systemIndex2 = systemIndexes.nextSetBit(systemIndex2 + 1);
                            final Object o2;
                            if (o2 >= 0) {
                                final BlockModule.MigrationSystem system2 = data.getSystem(systemIndex2, systemType);
                                if (system2.test(registry, holder.getArchetype())) {
                                    system2.onEntityRemoved(holder, RemoveReason.UNLOAD, store.getStore());
                                }
                                else {
                                    continue;
                                }
                            }
                            else {
                                break;
                            }
                        }
                        if (destructive) {
                            holder.tryRemoveComponent(registry.getUnknownComponentType());
                        }
                        if (!holder.hasSerializableComponents(data)) {
                            return new BlockHolder(b.blockId(), b.rotation(), b.filler(), b.supportValue(), null);
                        }
                        else {
                            return new BlockHolder(b.blockId(), b.rotation(), b.filler(), b.supportValue(), holder.clone());
                        }
                    }
                    catch (final Throwable e) {
                        throw new RuntimeException("Failed to read block state: " + String.valueOf(b), e);
                    }
                }
            });
        }
        finally {
            this.blocksLock.writeLock().unlock();
        }
    }
    
    public void addEntityFromWorld(@Nonnull final Holder<EntityStore> entityHolder) {
        final TransformComponent transformComponent = entityHolder.getComponent(TransformComponent.getComponentType());
        assert transformComponent != null;
        transformComponent.getPosition().subtract(this.x, this.y, this.z);
        this.addEntityHolderRaw(entityHolder);
    }
    
    public void addEntityHolderRaw(final Holder<EntityStore> entityHolder) {
        this.entitiesLock.writeLock().lock();
        try {
            this.entities.add(entityHolder);
        }
        finally {
            this.entitiesLock.writeLock().unlock();
        }
    }
    
    public void sortEntitiesByPosition() {
        this.entitiesLock.writeLock().lock();
        try {
            final ComponentType<EntityStore, TransformComponent> transformType = TransformComponent.getComponentType();
            this.entities.sort((a, b) -> {
                final TransformComponent ta = a.getComponent(transformType);
                final TransformComponent tb = b.getComponent(transformType);
                if (ta == null && tb == null) {
                    return 0;
                }
                else if (ta == null) {
                    return 1;
                }
                else if (tb == null) {
                    return -1;
                }
                else {
                    final Vector3d pa = ta.getPosition();
                    final Vector3d pb = tb.getPosition();
                    if (pa == null && pb == null) {
                        return 0;
                    }
                    else if (pa == null) {
                        return 1;
                    }
                    else if (pb == null) {
                        return -1;
                    }
                    else {
                        final int cmp = Double.compare(pa.getX(), pb.getX());
                        if (cmp != 0) {
                            return cmp;
                        }
                        else {
                            final int cmp2 = Double.compare(pa.getY(), pb.getY());
                            if (cmp2 != 0) {
                                return cmp2;
                            }
                            else {
                                return Double.compare(pa.getZ(), pb.getZ());
                            }
                        }
                    }
                }
            });
        }
        finally {
            this.entitiesLock.writeLock().unlock();
        }
    }
    
    public void placeNoReturn(@Nonnull final World world, final Vector3i position, final ComponentAccessor<EntityStore> componentAccessor) {
        this.placeNoReturn(null, null, FeedbackConsumer.DEFAULT, world, position, null, componentAccessor);
    }
    
    public void placeNoReturn(final String feedbackKey, final CommandSender feedback, @Nonnull final World outerWorld, final ComponentAccessor<EntityStore> componentAccessor) {
        this.placeNoReturn(feedbackKey, feedback, FeedbackConsumer.DEFAULT, outerWorld, Vector3i.ZERO, null, componentAccessor);
    }
    
    public void placeNoReturn(final String feedbackKey, final CommandSender feedback, @Nonnull final FeedbackConsumer feedbackConsumer, @Nonnull final World outerWorld, final ComponentAccessor<EntityStore> componentAccessor) {
        this.placeNoReturn(feedbackKey, feedback, feedbackConsumer, outerWorld, Vector3i.ZERO, null, componentAccessor);
    }
    
    public void placeNoReturn(@Nullable final String feedbackKey, @Nullable final CommandSender feedback, @Nonnull final FeedbackConsumer feedbackConsumer, @Nonnull final World outerWorld, @Nullable final Vector3i position, @Nullable final BlockMask blockMask, final ComponentAccessor<EntityStore> componentAccessor) {
        IntUnaryOperator xConvert;
        if (position != null && position.getX() != 0) {
            xConvert = (localX -> localX + this.x + position.getX() - this.anchorX);
        }
        else {
            xConvert = (localX -> localX + this.x - this.anchorX);
        }
        IntUnaryOperator yConvert;
        if (position != null && position.getY() != 0) {
            yConvert = (localY -> localY + this.y + position.getY() - this.anchorY);
        }
        else {
            yConvert = (localY -> localY + this.y - this.anchorY);
        }
        IntUnaryOperator zConvert;
        if (position != null && position.getZ() != 0) {
            zConvert = (localZ -> localZ + this.z + position.getZ() - this.anchorZ);
        }
        else {
            zConvert = (localZ -> localZ + this.z - this.anchorZ);
        }
        final LongSet dirtyChunks = new LongOpenHashSet();
        this.blocksLock.readLock().lock();
        try {
            final BlockTypeAssetMap<String, BlockType> assetMap = BlockType.getAssetMap();
            final int totalBlocks = this.blocks.size();
            final AtomicInteger counter = new AtomicInteger();
            outerWorld.getBlockBulkRelative(this.blocks, xConvert, yConvert, zConvert, (world, blockHolder, chunkIndex, chunk, blockX, blockY, blockZ, localX, localY, localZ) -> {
                final int newBlockId = blockHolder.blockId();
                final Holder<ChunkStore> holder = blockHolder.holder();
                this.placeBlockNoReturn(feedbackKey, feedback, feedbackConsumer, outerWorld, blockMask, dirtyChunks, assetMap, totalBlocks, counter.incrementAndGet(), chunkIndex, chunk, blockX, blockY, blockZ, newBlockId, blockHolder.rotation(), blockHolder.filler(), (holder != null) ? holder.clone() : null, componentAccessor);
                return;
            });
            outerWorld.getBlockBulkRelative(this.fluids, xConvert, yConvert, zConvert, (world, fluidStore, chunkIndex, chunk, blockX, blockY, blockZ, localX, localY, localZ) -> this.placeFluidNoReturn(feedbackKey, feedback, feedbackConsumer, outerWorld, blockMask, dirtyChunks, assetMap, totalBlocks, counter.incrementAndGet(), chunkIndex, chunk, blockX, blockY, blockZ, fluidStore.fluidId, fluidStore.fluidLevel, componentAccessor));
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
        dirtyChunks.forEach(value -> outerWorld.getChunkLighting().invalidateLightInChunk(outerWorld.getChunkIfInMemory(value)));
        this.placeEntities(outerWorld, position);
        dirtyChunks.forEach(value -> outerWorld.getNotificationHandler().updateChunk(value));
    }
    
    private void placeBlockNoReturn(final String feedbackKey, final CommandSender feedback, @Nonnull final FeedbackConsumer feedbackConsumer, @Nonnull final World outerWorld, @Nullable final BlockMask blockMask, @Nonnull final LongSet dirtyChunks, @Nonnull final BlockTypeAssetMap<String, BlockType> assetMap, final int totalBlocks, final int counter, final long chunkIndex, @Nonnull final WorldChunk chunk, final int blockX, final int blockY, final int blockZ, final int newBlockId, final int newRotation, final int newFiller, final Holder<ChunkStore> holder, final ComponentAccessor<EntityStore> componentAccessor) {
        if (blockY < 0 || blockY >= 320) {
            return;
        }
        final int oldBlockId = chunk.getBlock(blockX, blockY, blockZ);
        if (blockMask != null && blockMask.isExcluded(outerWorld, blockX, blockY, blockZ, this.min, this.max, oldBlockId)) {
            return;
        }
        final BlockChunk blockChunk = chunk.getBlockChunk();
        if (blockChunk.setBlock(blockX, blockY, blockZ, newBlockId, newRotation, newFiller)) {
            final BlockType newBlockType = assetMap.getAsset(newBlockId);
            if (newBlockType != null && FluidTicker.isFullySolid(newBlockType)) {
                this.clearFluidAtPosition(outerWorld, chunk, blockX, blockY, blockZ);
            }
            final short height = blockChunk.getHeight(blockX, blockZ);
            if (height <= blockY) {
                if (height == blockY && newBlockId == 0) {
                    blockChunk.updateHeight(blockX, blockZ, (short)blockY);
                }
                else if (height < blockY && newBlockId != 0 && newBlockType != null && newBlockType.getOpacity() != Opacity.Transparent) {
                    blockChunk.setHeight(blockX, blockZ, (short)blockY);
                }
            }
        }
        chunk.setState(blockX, blockY, blockZ, holder);
        dirtyChunks.add(chunkIndex);
        feedbackConsumer.accept(feedbackKey, totalBlocks, counter, feedback, componentAccessor);
    }
    
    private void placeFluidNoReturn(final String feedbackKey, final CommandSender feedback, @Nonnull final FeedbackConsumer feedbackConsumer, @Nonnull final World outerWorld, final BlockMask blockMask, @Nonnull final LongSet dirtyChunks, final BlockTypeAssetMap<String, BlockType> assetMap, final int totalBlocks, final int counter, final long chunkIndex, @Nonnull final WorldChunk chunk, final int blockX, final int blockY, final int blockZ, final int newFluidId, final byte newFluidLevel, final ComponentAccessor<EntityStore> componentAccessor) {
        if (blockY < 0 || blockY >= 320) {
            return;
        }
        final int sectionY = ChunkUtil.chunkCoordinate(blockY);
        final Store<ChunkStore> store = outerWorld.getChunkStore().getStore();
        final ChunkColumn column = store.getComponent(chunk.getReference(), ChunkColumn.getComponentType());
        final Ref<ChunkStore> section = column.getSection(sectionY);
        final FluidSection fluidSection = store.ensureAndGetComponent(section, FluidSection.getComponentType());
        fluidSection.setFluid(blockX, blockY, blockZ, newFluidId, newFluidLevel);
        dirtyChunks.add(chunkIndex);
        feedbackConsumer.accept(feedbackKey, totalBlocks, counter, feedback, componentAccessor);
    }
    
    private void clearFluidAtPosition(@Nonnull final World world, @Nonnull final WorldChunk chunk, final int blockX, final int blockY, final int blockZ) {
        final Ref<ChunkStore> ref = chunk.getReference();
        if (ref == null || !ref.isValid()) {
            return;
        }
        final Store<ChunkStore> store = world.getChunkStore().getStore();
        final ChunkColumn column = store.getComponent(ref, ChunkColumn.getComponentType());
        if (column == null) {
            return;
        }
        final Ref<ChunkStore> section = column.getSection(ChunkUtil.chunkCoordinate(blockY));
        if (section == null) {
            return;
        }
        final FluidSection fluidSection = store.getComponent(section, FluidSection.getComponentType());
        if (fluidSection != null) {
            fluidSection.setFluid(blockX, blockY, blockZ, 0, (byte)0);
        }
    }
    
    @Nonnull
    public BlockSelection place(final CommandSender feedback, @Nonnull final World outerWorld) {
        return this.place(feedback, outerWorld, Vector3i.ZERO, null);
    }
    
    @Nonnull
    public BlockSelection place(final CommandSender feedback, @Nonnull final World outerWorld, final BlockMask blockMask) {
        return this.place(feedback, outerWorld, Vector3i.ZERO, blockMask);
    }
    
    @Nonnull
    public BlockSelection place(final CommandSender feedback, @Nonnull final World outerWorld, final Vector3i position, final BlockMask blockMask) {
        return this.place(feedback, outerWorld, position, blockMask, BlockSelection.DEFAULT_ENTITY_CONSUMER);
    }
    
    @Nonnull
    public BlockSelection place(final CommandSender feedback, @Nonnull final World outerWorld, @Nullable final Vector3i position, @Nullable final BlockMask blockMask, @Nonnull final Consumer<Ref<EntityStore>> entityConsumer) {
        final BlockSelection before = new BlockSelection(this.getBlockCount(), 0);
        before.setAnchor(this.anchorX, this.anchorY, this.anchorZ);
        before.setPosition(this.x, this.y, this.z);
        IntUnaryOperator xConvert;
        if (position != null && position.getX() != 0) {
            xConvert = (localX -> localX + this.x + position.getX() - this.anchorX);
        }
        else {
            xConvert = (localX -> localX + this.x - this.anchorX);
        }
        IntUnaryOperator yConvert;
        if (position != null && position.getY() != 0) {
            yConvert = (localY -> localY + this.y + position.getY() - this.anchorY);
        }
        else {
            yConvert = (localY -> localY + this.y - this.anchorY);
        }
        IntUnaryOperator zConvert;
        if (position != null && position.getZ() != 0) {
            zConvert = (localZ -> localZ + this.z + position.getZ() - this.anchorZ);
        }
        else {
            zConvert = (localZ -> localZ + this.z - this.anchorZ);
        }
        final LongSet dirtyChunks = new LongOpenHashSet();
        this.blocksLock.readLock().lock();
        try {
            final BlockTypeAssetMap<String, BlockType> assetMap = BlockType.getAssetMap();
            outerWorld.getBlockBulkRelative(this.blocks, xConvert, yConvert, zConvert, (world, blockHolder, chunkIndex, chunk, blockX, blockY, blockZ, localX, localY, localZ) -> {
                final Holder<ChunkStore> holder = blockHolder.holder();
                this.placeBlock(feedback, outerWorld, blockMask, before, dirtyChunks, assetMap, chunkIndex, chunk, blockX, blockY, blockZ, localX, localY, localZ, blockHolder.blockId(), blockHolder.rotation(), blockHolder.filler(), (holder != null) ? holder.clone() : null, blockHolder.supportValue());
                return;
            });
            final IndexedLookupTableAssetMap<String, Fluid> fluidMap = Fluid.getAssetMap();
            outerWorld.getBlockBulkRelative(this.fluids, xConvert, yConvert, zConvert, (world, fluidStore, chunkIndex, chunk, blockX, blockY, blockZ, localX, localY, localZ) -> this.placeFluid(feedback, outerWorld, before, dirtyChunks, fluidMap, chunkIndex, chunk, blockX, blockY, blockZ, localX, localY, localZ, fluidStore.fluidId, fluidStore.fluidLevel));
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
        dirtyChunks.forEach(value -> outerWorld.getChunkLighting().invalidateLightInChunk(outerWorld.getChunkIfInMemory(value)));
        this.placeEntities(outerWorld, position, entityConsumer);
        dirtyChunks.forEach(value -> outerWorld.getNotificationHandler().updateChunk(value));
        return before;
    }
    
    private void placeBlock(final CommandSender feedback, @Nonnull final World outerWorld, @Nullable final BlockMask blockMask, @Nonnull final BlockSelection before, @Nonnull final LongSet dirtyChunks, @Nonnull final BlockTypeAssetMap<String, BlockType> assetMap, final long chunkIndex, @Nonnull final WorldChunk chunk, final int blockX, final int blockY, final int blockZ, final int localX, final int localY, final int localZ, final int newBlockId, final int newRotation, final int newFiller, final Holder<ChunkStore> holder, final int newSupportValue) {
        if (blockY < 0 || blockY >= 320) {
            return;
        }
        final Store<ChunkStore> chunkStore = chunk.getWorld().getChunkStore().getStore();
        final ChunkColumn chunkColumn = chunkStore.getComponent(chunk.getReference(), ChunkColumn.getComponentType());
        final Ref<ChunkStore> section = chunkColumn.getSection(ChunkUtil.chunkCoordinate(blockY));
        final BlockSection blockSection = chunkStore.getComponent(section, BlockSection.getComponentType());
        final int oldBlockId = chunk.getBlock(blockX, blockY, blockZ);
        if (blockMask != null && blockMask.isExcluded(outerWorld, blockX, blockY, blockZ, this.min, this.max, oldBlockId)) {
            return;
        }
        BlockPhysics blockPhysics = (section != null) ? chunkStore.getComponent(section, BlockPhysics.getComponentType()) : null;
        final int supportValue = (blockPhysics != null) ? blockPhysics.get(blockX, blockY, blockZ) : 0;
        final int filler = blockSection.getFiller(blockX, blockY, blockZ);
        final int rotation = blockSection.getRotationIndex(blockX, blockY, blockZ);
        before.addBlockAtLocalPos(localX, localY, localZ, oldBlockId, rotation, filler, supportValue, chunk.getBlockComponentHolder(blockX, blockY, blockZ));
        final BlockChunk blockChunk = chunk.getBlockChunk();
        if (blockChunk.setBlock(blockX, blockY, blockZ, newBlockId, newRotation, newFiller)) {
            final BlockType newBlockType = assetMap.getAsset(newBlockId);
            if (newBlockType != null && FluidTicker.isFullySolid(newBlockType)) {
                this.clearFluidAtPosition(outerWorld, chunk, blockX, blockY, blockZ);
            }
            final short height = blockChunk.getHeight(blockX, blockZ);
            if (height <= blockY) {
                if (height == blockY && newBlockId == 0) {
                    blockChunk.updateHeight(blockX, blockZ, (short)blockY);
                }
                else if (height < blockY && newBlockId != 0 && newBlockType.getOpacity() != Opacity.Transparent) {
                    blockChunk.setHeight(blockX, blockZ, (short)blockY);
                }
            }
            if (newSupportValue != supportValue) {
                if (newSupportValue != 0) {
                    if (blockPhysics == null) {
                        blockPhysics = chunkStore.ensureAndGetComponent(section, BlockPhysics.getComponentType());
                    }
                    blockPhysics.set(blockX, blockY, blockZ, newSupportValue);
                }
                else if (blockPhysics != null) {
                    blockPhysics.set(blockX, blockY, blockZ, 0);
                }
            }
        }
        chunk.setState(blockX, blockY, blockZ, holder);
        dirtyChunks.add(chunkIndex);
    }
    
    private void placeFluid(final CommandSender feedback, @Nonnull final World outerWorld, @Nonnull final BlockSelection before, @Nonnull final LongSet dirtyChunks, final IndexedLookupTableAssetMap<String, Fluid> assetMap, final long chunkIndex, @Nonnull final WorldChunk chunk, final int blockX, final int blockY, final int blockZ, final int localX, final int localY, final int localZ, final int newFluidId, final byte newFluidLevel) {
        if (blockY < 0 || blockY >= 320) {
            return;
        }
        final int sectionY = ChunkUtil.chunkCoordinate(blockY);
        final Store<ChunkStore> store = outerWorld.getChunkStore().getStore();
        final ChunkColumn column = store.getComponent(chunk.getReference(), ChunkColumn.getComponentType());
        final Ref<ChunkStore> section = column.getSection(sectionY);
        final FluidSection fluidSection = store.ensureAndGetComponent(section, FluidSection.getComponentType());
        final int oldFluidId = fluidSection.getFluidId(blockX, blockY, blockZ);
        final byte oldFluidLevel = fluidSection.getFluidLevel(blockX, blockY, blockZ);
        before.addFluidAtLocalPos(localX, localY, localZ, oldFluidId, oldFluidLevel);
        fluidSection.setFluid(blockX, blockY, blockZ, newFluidId, newFluidLevel);
        dirtyChunks.add(chunkIndex);
    }
    
    private void placeEntities(@Nonnull final World world, @Nonnull final Vector3i pos) {
        this.placeEntities(world, pos, BlockSelection.DEFAULT_ENTITY_CONSUMER);
    }
    
    private void placeEntities(@Nonnull final World world, @Nonnull final Vector3i pos, @Nonnull final Consumer<Ref<EntityStore>> entityConsumer) {
        this.entitiesLock.readLock().lock();
        try {
            for (final Holder<EntityStore> entityHolder : this.entities) {
                final Ref<EntityStore> entity = this.placeEntity(world, entityHolder.clone(), pos, this.prefabId);
                if (entity == null) {
                    BlockSelection.LOGGER.at(Level.WARNING).log("Failed to spawn entity in world %s! Data: %s", world.getName(), entityHolder);
                }
                else {
                    entityConsumer.accept(entity);
                }
            }
        }
        finally {
            this.entitiesLock.readLock().unlock();
        }
    }
    
    @Nonnull
    private Ref<EntityStore> placeEntity(@Nonnull final World world, @Nonnull final Holder<EntityStore> entityHolder, @Nonnull final Vector3i pos, final int prefabId) {
        final TransformComponent transformComponent = entityHolder.getComponent(TransformComponent.getComponentType());
        assert transformComponent != null;
        transformComponent.getPosition().add(this.x + pos.getX() - this.anchorX, this.y + pos.getY() - this.anchorY, this.z + pos.getZ() - this.anchorZ);
        final Store<EntityStore> store = world.getEntityStore().getStore();
        final PrefabPlaceEntityEvent prefabPlaceEntityEvent = new PrefabPlaceEntityEvent(prefabId, entityHolder);
        store.invoke(prefabPlaceEntityEvent);
        entityHolder.addComponent(FromPrefab.getComponentType(), FromPrefab.INSTANCE);
        final Ref<EntityStore> entityRef = new Ref<EntityStore>(store);
        world.execute(() -> store.addEntity(entityHolder, entityRef, AddReason.LOAD));
        return entityRef;
    }
    
    @Nonnull
    public BlockSelection rotate(@Nonnull final Axis axis, final int angle) {
        final BlockTypeAssetMap<String, BlockType> assetMap = BlockType.getAssetMap();
        final BlockSelection selection = new BlockSelection(this.getBlockCount(), this.getEntityCount());
        selection.copyPropertiesFrom(this);
        final Vector3i mutable = new Vector3i(0, 0, 0);
        final Rotation rotation = Rotation.ofDegrees(angle);
        this.forEachBlock((x1, y1, z1, block) -> {
            mutable.assign(x1 - this.anchorX, y1 - this.anchorY, z1 - this.anchorZ);
            axis.rotate(mutable, angle);
            final int blockId = block.blockId;
            final Holder<ChunkStore> holder = block.holder;
            final RotationTuple blockRotation = RotationTuple.get(block.rotation);
            switch (axis) {
                default: {
                    throw new MatchException(null, null);
                }
                case X: {
                    RotationTuple.of(blockRotation.yaw(), blockRotation.pitch().add(rotation), blockRotation.roll());
                    break;
                }
                case Y: {
                    RotationTuple.of(blockRotation.yaw().add(rotation), blockRotation.pitch(), blockRotation.roll());
                    break;
                }
                case Z: {
                    RotationTuple.of(blockRotation.yaw(), blockRotation.pitch(), blockRotation.roll().add(rotation));
                    break;
                }
            }
            final RotationTuple rotationTuple;
            RotationTuple rotatedRotation = rotationTuple;
            if (rotatedRotation == null) {
                rotatedRotation = blockRotation;
            }
            final int rotatedFiller = BlockRotationUtil.getRotatedFiller(block.filler, axis, rotation);
            selection.addBlock0(mutable.getX() + this.anchorX, mutable.getY() + this.anchorY, mutable.getZ() + this.anchorZ, blockId, rotatedRotation.index(), rotatedFiller, block.supportValue(), (holder != null) ? holder.clone() : null);
            return;
        });
        this.forEachEntity(entityHolder -> {
            final Holder<EntityStore> copy = entityHolder.clone();
            final TransformComponent transformComponent = copy.getComponent(TransformComponent.getComponentType());
            if (!BlockSelection.$assertionsDisabled && transformComponent == null) {
                throw new AssertionError();
            }
            else {
                final Vector3d position = transformComponent.getPosition();
                final HeadRotation headRotationComponent = copy.getComponent(HeadRotation.getComponentType());
                position.subtract(this.anchorX, this.anchorY, this.anchorZ).subtract(0.5, 0.0, 0.5);
                axis.rotate(position, angle);
                position.add(this.anchorX, this.anchorY, this.anchorZ).add(0.5, 0.0, 0.5);
                transformComponent.getRotation().addRotationOnAxis(axis, angle);
                if (headRotationComponent != null) {
                    headRotationComponent.getRotation().addRotationOnAxis(axis, angle);
                }
                selection.addEntity0(copy);
                return;
            }
        });
        return selection;
    }
    
    @Nonnull
    public BlockSelection rotate(@Nonnull final Axis axis, final int angle, @Nonnull final Vector3f originOfRotation) {
        final BlockSelection selection = new BlockSelection(this.getBlockCount(), this.getEntityCount());
        selection.copyPropertiesFrom(this);
        final Vector3d mutable = new Vector3d(0.0, 0.0, 0.0);
        final Rotation rotation = Rotation.ofDegrees(angle);
        final Vector3f finalOriginOfRotation = originOfRotation.clone().subtract((float)this.x, (float)this.y, (float)this.z);
        this.forEachBlock((x1, y1, z1, block) -> {
            mutable.assign(x1 - finalOriginOfRotation.x, y1 - finalOriginOfRotation.y, z1 - finalOriginOfRotation.z);
            axis.rotate(mutable, angle);
            final int blockId = block.blockId;
            final Holder<ChunkStore> holder = block.holder;
            final int supportValue = block.supportValue();
            final RotationTuple blockRotation = RotationTuple.get(block.rotation);
            switch (axis) {
                default: {
                    throw new MatchException(null, null);
                }
                case X: {
                    RotationTuple.of(blockRotation.yaw(), blockRotation.pitch().add(rotation), blockRotation.roll());
                    break;
                }
                case Y: {
                    RotationTuple.of(blockRotation.yaw().add(rotation), blockRotation.pitch(), blockRotation.roll());
                    break;
                }
                case Z: {
                    RotationTuple.of(blockRotation.yaw(), blockRotation.pitch(), blockRotation.roll().add(rotation));
                    break;
                }
            }
            final RotationTuple rotationTuple;
            RotationTuple rotatedRotation = rotationTuple;
            if (rotatedRotation == null) {
                rotatedRotation = blockRotation;
            }
            final int rotatedFiller = BlockRotationUtil.getRotatedFiller(block.filler, axis, rotation);
            selection.addBlock0((int)(mutable.getX() + finalOriginOfRotation.x), (int)(mutable.getY() + finalOriginOfRotation.z), (int)(mutable.getZ() + finalOriginOfRotation.z), blockId, rotatedRotation.index(), rotatedFiller, supportValue, (holder != null) ? holder.clone() : null);
            return;
        });
        this.forEachEntity(entityHolder -> {
            final Holder<EntityStore> copy = entityHolder.clone();
            final TransformComponent transformComponent = copy.getComponent(TransformComponent.getComponentType());
            if (!BlockSelection.$assertionsDisabled && transformComponent == null) {
                throw new AssertionError();
            }
            else {
                final Vector3d position = transformComponent.getPosition();
                final HeadRotation headRotationComponent = copy.getComponent(HeadRotation.getComponentType());
                position.subtract(this.anchorX, this.anchorY, this.anchorZ).subtract(0.5, 0.0, 0.5);
                axis.rotate(position, angle);
                position.add(this.anchorX, this.anchorY, this.anchorZ).add(0.5, 0.0, 0.5);
                transformComponent.getRotation().addRotationOnAxis(axis, angle);
                if (headRotationComponent != null) {
                    headRotationComponent.getRotation().addRotationOnAxis(axis, angle);
                }
                selection.addEntity0(copy);
                return;
            }
        });
        return selection;
    }
    
    @Nonnull
    public BlockSelection rotateArbitrary(final float yawDegrees, final float pitchDegrees, final float rollDegrees) {
        final double pitchRad = Math.toRadians(pitchDegrees);
        final double yawRad = Math.toRadians(yawDegrees);
        final double rollRad = Math.toRadians(rollDegrees);
        final Matrix4d rotation = new Matrix4d();
        rotation.setRotateEuler(pitchRad, yawRad, rollRad);
        final Matrix4d inverse = new Matrix4d(rotation);
        inverse.invert();
        final Vector3d tempVec = new Vector3d();
        int destMinX = Integer.MAX_VALUE;
        int destMinY = Integer.MAX_VALUE;
        int destMinZ = Integer.MAX_VALUE;
        int destMaxX = Integer.MIN_VALUE;
        int destMaxY = Integer.MIN_VALUE;
        int destMaxZ = Integer.MIN_VALUE;
        int srcMinX = Integer.MAX_VALUE;
        int srcMinY = Integer.MAX_VALUE;
        int srcMinZ = Integer.MAX_VALUE;
        int srcMaxX = Integer.MIN_VALUE;
        int srcMaxY = Integer.MIN_VALUE;
        int srcMaxZ = Integer.MIN_VALUE;
        this.blocksLock.readLock().lock();
        try {
            for (final Long2ObjectMap.Entry<BlockHolder> entry : this.blocks.long2ObjectEntrySet()) {
                final long packed = entry.getLongKey();
                final int bx = BlockUtil.unpackX(packed) - this.anchorX;
                final int by = BlockUtil.unpackY(packed) - this.anchorY;
                final int bz = BlockUtil.unpackZ(packed) - this.anchorZ;
                srcMinX = Math.min(srcMinX, bx);
                srcMinY = Math.min(srcMinY, by);
                srcMinZ = Math.min(srcMinZ, bz);
                srcMaxX = Math.max(srcMaxX, bx);
                srcMaxY = Math.max(srcMaxY, by);
                srcMaxZ = Math.max(srcMaxZ, bz);
            }
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
        BlockSelection selection = null;
        if (srcMinX == Integer.MAX_VALUE) {
            selection = new BlockSelection(0, this.getEntityCount());
            selection.copyPropertiesFrom(this);
            return selection;
        }
        final int[][] array;
        final int[][] corners = array = new int[][] { { srcMinX, srcMinY, srcMinZ }, { srcMaxX, srcMinY, srcMinZ }, { srcMinX, srcMaxY, srcMinZ }, { srcMaxX, srcMaxY, srcMinZ }, { srcMinX, srcMinY, srcMaxZ }, { srcMaxX, srcMinY, srcMaxZ }, { srcMinX, srcMaxY, srcMaxZ }, { srcMaxX, srcMaxY, srcMaxZ } };
        for (final int[] corner : array) {
            tempVec.assign(corner[0], corner[1], corner[2]);
            rotation.multiplyDirection(tempVec);
            final int rx = MathUtil.floor(tempVec.x);
            final int ry = MathUtil.floor(tempVec.y);
            final int rz = MathUtil.floor(tempVec.z);
            destMinX = Math.min(destMinX, rx);
            destMinY = Math.min(destMinY, ry);
            destMinZ = Math.min(destMinZ, rz);
            destMaxX = Math.max(destMaxX, rx + 1);
            destMaxY = Math.max(destMaxY, ry + 1);
            destMaxZ = Math.max(destMaxZ, rz + 1);
        }
        final BlockSelection selection2 = new BlockSelection(this.getBlockCount(), this.getEntityCount());
        selection2.copyPropertiesFrom(this);
        final Rotation snappedYaw = Rotation.ofDegrees(Math.round(yawDegrees / 90.0f) * 90);
        final Rotation snappedPitch = Rotation.ofDegrees(Math.round(pitchDegrees / 90.0f) * 90);
        final Rotation snappedRoll = Rotation.ofDegrees(Math.round(rollDegrees / 90.0f) * 90);
        this.blocksLock.readLock().lock();
        try {
            for (int dx = destMinX; dx <= destMaxX; ++dx) {
                for (int dy = destMinY; dy <= destMaxY; ++dy) {
                    for (int dz = destMinZ; dz <= destMaxZ; ++dz) {
                        tempVec.assign(dx, dy, dz);
                        inverse.multiplyDirection(tempVec);
                        final int sx = (int)Math.round(tempVec.x);
                        final int sy = (int)Math.round(tempVec.y);
                        final int sz = (int)Math.round(tempVec.z);
                        final long packedSource = BlockUtil.pack(sx + this.anchorX, sy + this.anchorY, sz + this.anchorZ);
                        final BlockHolder block = this.blocks.get(packedSource);
                        if (block != null) {
                            final RotationTuple blockRotation = RotationTuple.get(block.rotation());
                            RotationTuple rotatedRotation = RotationTuple.of(blockRotation.yaw().add(snappedYaw), blockRotation.pitch().add(snappedPitch), blockRotation.roll().add(snappedRoll));
                            if (rotatedRotation == null) {
                                rotatedRotation = blockRotation;
                            }
                            int rotatedFiller = block.filler();
                            if (rotatedFiller != 0) {
                                final int fillerX = FillerBlockUtil.unpackX(rotatedFiller);
                                final int fillerY = FillerBlockUtil.unpackY(rotatedFiller);
                                final int fillerZ = FillerBlockUtil.unpackZ(rotatedFiller);
                                tempVec.assign(fillerX, fillerY, fillerZ);
                                rotation.multiplyDirection(tempVec);
                                rotatedFiller = FillerBlockUtil.pack((int)Math.round(tempVec.x), (int)Math.round(tempVec.y), (int)Math.round(tempVec.z));
                            }
                            final Holder<ChunkStore> holder = block.holder();
                            selection2.addBlock0(dx + this.anchorX, dy + this.anchorY, dz + this.anchorZ, block.blockId(), rotatedRotation.index(), rotatedFiller, block.supportValue(), (holder != null) ? holder.clone() : null);
                        }
                    }
                }
            }
            for (int dx = destMinX; dx <= destMaxX; ++dx) {
                for (int dy = destMinY; dy <= destMaxY; ++dy) {
                    for (int dz = destMinZ; dz <= destMaxZ; ++dz) {
                        tempVec.assign(dx, dy, dz);
                        inverse.multiplyDirection(tempVec);
                        final int sx = (int)Math.round(tempVec.x);
                        final int sy = (int)Math.round(tempVec.y);
                        final int sz = (int)Math.round(tempVec.z);
                        final long packedSource = BlockUtil.pack(sx + this.anchorX, sy + this.anchorY, sz + this.anchorZ);
                        final FluidHolder fluid = this.fluids.get(packedSource);
                        if (fluid != null) {
                            selection2.addFluid0(dx + this.anchorX, dy + this.anchorY, dz + this.anchorZ, fluid.fluidId(), fluid.fluidLevel());
                        }
                    }
                }
            }
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
        final float yawRadF = (float)yawRad;
        final float pitchRadF = (float)pitchRad;
        final float rollRadF = (float)rollRad;
        this.forEachEntity(entityHolder -> {
            final Holder<EntityStore> copy = entityHolder.clone();
            final TransformComponent transformComponent = copy.getComponent(TransformComponent.getComponentType());
            if (!BlockSelection.$assertionsDisabled && transformComponent == null) {
                throw new AssertionError();
            }
            else {
                final Vector3d position = transformComponent.getPosition();
                final HeadRotation headRotationComp = copy.getComponent(HeadRotation.getComponentType());
                position.subtract(this.anchorX, this.anchorY, this.anchorZ).subtract(0.5, 0.0, 0.5);
                rotation.multiplyDirection(position);
                position.add(this.anchorX, this.anchorY, this.anchorZ).add(0.5, 0.0, 0.5);
                final Vector3f bodyRotation = transformComponent.getRotation();
                bodyRotation.addPitch(pitchRadF);
                bodyRotation.addYaw(yawRadF);
                bodyRotation.addRoll(rollRadF);
                if (headRotationComp != null) {
                    final Vector3f headRot = headRotationComp.getRotation();
                    headRot.addPitch(pitchRadF);
                    headRot.addYaw(yawRadF);
                    headRot.addRoll(rollRadF);
                }
                selection.addEntity0(copy);
                return;
            }
        });
        return selection2;
    }
    
    @Nonnull
    public BlockSelection flip(@Nonnull final Axis axis) {
        final BlockTypeAssetMap<String, BlockType> assetMap = BlockType.getAssetMap();
        final BlockSelection selection = new BlockSelection(this.getBlockCount(), this.getEntityCount());
        selection.copyPropertiesFrom(this);
        final Vector3i mutable = new Vector3i(0, 0, 0);
        this.forEachBlock((x1, y1, z1, block) -> {
            mutable.assign(x1 - this.anchorX, y1 - this.anchorY, z1 - this.anchorZ);
            axis.flip(mutable);
            final int blockId = block.blockId;
            final Holder<ChunkStore> holder = block.holder;
            final int supportValue = block.supportValue();
            final int filler = block.filler;
            final BlockType blockType = assetMap.getAsset(blockId);
            final VariantRotation variantRotation = blockType.getVariantRotation();
            if (variantRotation == VariantRotation.None) {
                selection.addBlock0(mutable.getX() + this.anchorX, mutable.getY() + this.anchorY, mutable.getZ() + this.anchorZ, block);
                return;
            }
            else {
                final RotationTuple blockRotation = RotationTuple.get(block.rotation);
                RotationTuple rotatedRotation = BlockRotationUtil.getFlipped(blockRotation, blockType.getFlipType(), axis, variantRotation);
                if (rotatedRotation != null) {
                    rotatedRotation = blockRotation;
                }
                final int rotatedFiller = BlockRotationUtil.getFlippedFiller(filler, axis);
                selection.addBlock0(mutable.getX() + this.anchorX, mutable.getY() + this.anchorY, mutable.getZ() + this.anchorZ, blockId, rotatedRotation.index(), rotatedFiller, supportValue, (holder != null) ? holder.clone() : null);
                return;
            }
        });
        this.forEachEntity(entityHolder -> {
            final Holder<EntityStore> copy = entityHolder.clone();
            final HeadRotation headRotationComponent = copy.getComponent(HeadRotation.getComponentType());
            if (!BlockSelection.$assertionsDisabled && headRotationComponent == null) {
                throw new AssertionError();
            }
            else {
                final Vector3f headRotation = headRotationComponent.getRotation();
                final TransformComponent transformComponent = copy.getComponent(TransformComponent.getComponentType());
                if (!BlockSelection.$assertionsDisabled && transformComponent == null) {
                    throw new AssertionError();
                }
                else {
                    final Vector3d position = transformComponent.getPosition();
                    final Vector3f bodyRotation = transformComponent.getRotation();
                    position.subtract(this.anchorX, this.anchorY, this.anchorZ).subtract(0.5, 0.0, 0.5);
                    axis.flip(position);
                    position.add(this.anchorX, this.anchorY, this.anchorZ).add(0.5, 0.0, 0.5);
                    axis.flipRotation(bodyRotation);
                    axis.flipRotation(headRotation);
                    selection.addEntity0(copy);
                    return;
                }
            }
        });
        return selection;
    }
    
    @Nonnull
    public BlockSelection relativize() {
        return this.relativize(this.anchorX, this.anchorY, this.anchorZ);
    }
    
    @Nonnull
    public BlockSelection relativize(final int originX, final int originY, final int originZ) {
        if (originX == 0 && originY == 0 && originZ == 0) {
            return this.cloneSelection();
        }
        final BlockSelection selection = new BlockSelection(this.getBlockCount(), this.getEntityCount());
        selection.setAnchor(this.anchorX - originX, this.anchorY - originY, this.anchorZ - originZ);
        selection.setPosition(this.x - originX, this.y - originY, this.z - originZ);
        selection.setSelectionArea(this.min.clone().subtract(originX, originY, originZ), this.max.clone().subtract(originX, originY, originZ));
        this.forEachBlock((x, y, z, block) -> selection.addBlock0(x - originX, y - originY, z - originZ, block));
        this.forEachEntity(holder -> {
            final Holder<EntityStore> copy = holder.clone();
            final TransformComponent transformComponent = copy.getComponent(TransformComponent.getComponentType());
            if (!BlockSelection.$assertionsDisabled && transformComponent == null) {
                throw new AssertionError();
            }
            else {
                transformComponent.getPosition().subtract(originX, originY, originZ);
                selection.addEntity0(copy);
                return;
            }
        });
        return selection;
    }
    
    @Nonnull
    public BlockSelection cloneSelection() {
        final BlockSelection selection = new BlockSelection(this.getBlockCount(), this.getEntityCount());
        selection.copyPropertiesFrom(this);
        this.blocksLock.readLock().lock();
        try {
            Long2ObjectMaps.fastForEach(this.blocks, entry -> selection.blocks.put(entry.getLongKey(), ((BlockHolder)entry.getValue()).cloneBlockHolder()));
            selection.fluids.putAll((Map<?, ?>)this.fluids);
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
        this.entitiesLock.readLock().lock();
        try {
            this.entities.forEach(holder -> selection.entities.add(holder.clone()));
        }
        finally {
            this.entitiesLock.readLock().unlock();
        }
        return selection;
    }
    
    public void add(@Nonnull final BlockSelection other) {
        this.entitiesLock.writeLock().lock();
        try {
            other.forEachEntity(holder -> {
                final Holder<EntityStore> copy = holder.clone();
                final TransformComponent transformComponent = copy.getComponent(TransformComponent.getComponentType());
                if (!BlockSelection.$assertionsDisabled && transformComponent == null) {
                    throw new AssertionError();
                }
                else {
                    transformComponent.getPosition().add(other.x, other.y, other.z).subtract(this.x, this.y, this.z);
                    this.addEntity0(copy);
                    return;
                }
            });
        }
        finally {
            this.entitiesLock.writeLock().unlock();
        }
        this.blocksLock.writeLock().lock();
        try {
            other.forEachBlock((x1, y1, z1, block) -> this.addBlock0(x1 + other.x - this.x, y1 + other.y - this.y, z1 + other.z - this.z, block));
            other.forEachFluid((x1, y1, z1, fluidId, fluidLevel) -> this.addFluid0(x1 + other.x - this.x, y1 + other.y - this.y, z1 + other.z - this.z, fluidId, fluidLevel));
        }
        finally {
            this.blocksLock.writeLock().unlock();
        }
    }
    
    @Nonnull
    @Override
    public MetricResults toMetricResults() {
        return BlockSelection.METRICS_REGISTRY.toMetricResults(this);
    }
    
    @Nonnull
    @Override
    public EditorBlocksChange toPacket() {
        final EditorBlocksChange packet = new EditorBlocksChange();
        this.blocksLock.readLock().lock();
        try {
            final int blockCount = this.getBlockCount();
            final List<BlockChange> blockList = new ObjectArrayList<BlockChange>(blockCount);
            this.forEachBlock((x1, y1, z1, block) -> {
                if (block.filler != 0) {
                    return;
                }
                else {
                    blockList.add(new BlockChange(x1 - this.anchorX, y1 - this.anchorY, z1 - this.anchorZ, block.blockId, (byte)block.rotation));
                    return;
                }
            });
            final List<FluidChange> fluidList = new ObjectArrayList<FluidChange>();
            this.forEachFluid((x1, y1, z1, fluidId, fluidLevel) -> {
                if (fluidId != 0) {
                    fluidList.add(new FluidChange(x1 - this.anchorX, y1 - this.anchorY, z1 - this.anchorZ, fluidId, fluidLevel));
                }
                return;
            });
            packet.blocksChange = blockList.toArray(BlockChange[]::new);
            packet.fluidsChange = fluidList.toArray(FluidChange[]::new);
            packet.advancedPreview = true;
            packet.blocksCount = blockCount;
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
        return packet;
    }
    
    @Nonnull
    public EditorBlocksChange toSelectionPacket() {
        final EditorBlocksChange packet = new EditorBlocksChange();
        final EditorSelection selection = new EditorSelection();
        if (this.min != null) {
            selection.minX = this.min.getX();
            selection.minY = this.min.getY();
            selection.minZ = this.min.getZ();
        }
        if (this.max != null) {
            selection.maxX = this.max.getX();
            selection.maxY = this.max.getY();
            selection.maxZ = this.max.getZ();
        }
        packet.selection = selection;
        return packet;
    }
    
    @Nonnull
    public EditorBlocksChange toPacketWithSelection() {
        final EditorBlocksChange packet = this.toPacket();
        if (this.min != null && this.max != null) {
            final EditorSelection selection = new EditorSelection();
            selection.minX = this.min.getX();
            selection.minY = this.min.getY();
            selection.minZ = this.min.getZ();
            selection.maxX = this.max.getX();
            selection.maxY = this.max.getY();
            selection.maxZ = this.max.getZ();
            packet.selection = selection;
        }
        return packet;
    }
    
    public void tryFixFiller(final boolean allowDestructive) {
        this.blocksLock.readLock().lock();
        LongOpenHashSet blockPositions;
        try {
            blockPositions = new LongOpenHashSet(this.blocks.keySet());
        }
        finally {
            this.blocksLock.readLock().unlock();
        }
        final BlockTypeAssetMap<String, BlockType> blockTypeAssetMap = BlockType.getAssetMap();
        final IndexedLookupTableAssetMap<String, BlockBoundingBoxes> hitboxAssetMap = BlockBoundingBoxes.getAssetMap();
        final LongIterator it = blockPositions.iterator();
        while (it.hasNext()) {
            final long packed = it.nextLong();
            final int x = BlockUtil.unpackX(packed);
            final int y = BlockUtil.unpackY(packed);
            final int z = BlockUtil.unpackZ(packed);
            final BlockHolder blockHolder = this.getBlockHolderAtLocalPos(x, y, z);
            if (blockHolder == null) {
                continue;
            }
            final int blockId = blockHolder.blockId;
            if (blockId == 0) {
                continue;
            }
            final BlockType blockType = blockTypeAssetMap.getAsset(blockId);
            if (blockType == null) {
                continue;
            }
            final String id = blockType.getId();
            if (blockHolder.filler != 0) {
                final int fillerX = FillerBlockUtil.unpackX(blockHolder.filler);
                final int fillerY = FillerBlockUtil.unpackY(blockHolder.filler);
                final int fillerZ = FillerBlockUtil.unpackZ(blockHolder.filler);
                final BlockHolder baseBlockHolder = this.getBlockHolderAtLocalPos(x - fillerX, y - fillerY, z - fillerZ);
                final BlockType baseBlock = blockTypeAssetMap.getAsset(baseBlockHolder.blockId);
                if (baseBlock == null) {
                    this.addBlockAtLocalPos(x, y, z, 0, 0, 0, 0);
                }
                else {
                    final String baseId = baseBlock.getId();
                    final BlockBoundingBoxes hitbox = hitboxAssetMap.getAsset(baseBlock.getHitboxTypeIndex());
                    if (hitbox == null) {
                        continue;
                    }
                    if (id.equals(baseId) && baseBlockHolder.rotation == blockHolder.rotation && hitbox.get(blockHolder.rotation).getBoundingBox().containsBlock(fillerX, fillerY, fillerZ)) {
                        continue;
                    }
                    this.addBlockAtLocalPos(x, y, z, 0, 0, 0, 0);
                }
            }
            else {
                final BlockBoundingBoxes hitbox2 = hitboxAssetMap.getAsset(blockType.getHitboxTypeIndex());
                if (hitbox2 == null) {
                    continue;
                }
                if (!hitbox2.protrudesUnitBox()) {
                    continue;
                }
                FillerBlockUtil.forEachFillerBlock(hitbox2.get(blockHolder.rotation), (x1, y1, z1) -> {
                    if (x1 != 0 || y1 != 0 || z1 != 0) {
                        final int worldX = x + x1;
                        final int worldY = y + y1;
                        final int worldZ = z + z1;
                        final BlockHolder fillerBlockHolder = this.getBlockHolderAtLocalPos(worldX, worldY, worldZ);
                        final BlockType fillerBlock = blockTypeAssetMap.getAsset(fillerBlockHolder.blockId);
                        final int filler = FillerBlockUtil.pack(x1, y1, z1);
                        if (fillerBlock == null || !fillerBlock.getId().equals(id) || filler != fillerBlockHolder.filler) {
                            if (!allowDestructive && fillerBlockHolder.blockId != 0) {
                                throw new IllegalArgumentException("Cannot replace " + fillerBlock.getId() + " with " + blockType.getId() + " in order to repair filler\n at " + worldX + ", " + worldY + ", " + worldZ + "\n base " + x + ", " + y + ", " + z);
                            }
                            else {
                                this.addBlockAtLocalPos(worldX, worldY, worldZ, blockId, blockHolder.rotation, filler, 0);
                            }
                        }
                    }
                });
            }
        }
    }
    
    public void reserializeEntities(@Nonnull final Store<EntityStore> store, final boolean destructive) throws IOException {
        this.entitiesLock.writeLock().lock();
        try {
            if (this.entities.isEmpty()) {
                return;
            }
            final ComponentRegistry<EntityStore> registry = EntityStore.REGISTRY;
            final ComponentRegistry.Data<EntityStore> data = registry.getData();
            final SystemType<EntityStore, EntityModule.MigrationSystem> systemType = EntityModule.get().getMigrationSystemType();
            final BitSet systemIndexes = data.getSystemIndexesForType(systemType);
            int systemIndex = -1;
            while ((systemIndex = systemIndexes.nextSetBit(systemIndex + 1)) >= 0) {
                final EntityModule.MigrationSystem system = data.getSystem(systemIndex, systemType);
                for (int i = 0; i < this.entities.size(); ++i) {
                    final Holder<EntityStore> holder = this.entities.get(i);
                    if (system.test(registry, holder.getArchetype())) {
                        system.onEntityAdd(holder, AddReason.LOAD, store);
                    }
                }
            }
            systemIndex = -1;
            while ((systemIndex = systemIndexes.nextSetBit(systemIndex + 1)) >= 0) {
                final EntityModule.MigrationSystem system = data.getSystem(systemIndex, systemType);
                for (int i = 0; i < this.entities.size(); ++i) {
                    final Holder<EntityStore> holder = this.entities.get(i);
                    if (system.test(registry, holder.getArchetype())) {
                        system.onEntityRemoved(holder, RemoveReason.UNLOAD, store);
                    }
                }
            }
            if (destructive) {
                for (int j = 0; j < this.entities.size(); ++j) {
                    final Holder<EntityStore> holder2 = this.entities.get(j);
                    holder2.tryRemoveComponent(registry.getUnknownComponentType());
                }
            }
        }
        finally {
            this.entitiesLock.writeLock().unlock();
        }
    }
    
    @Nonnull
    @Override
    public String toString() {
        return "BlockSelection{blocksLock=" + String.valueOf(this.blocksLock) + ", x=" + this.x + ", y=" + this.y + ", z=" + this.z + ", originX=" + this.anchorX + ", originY=" + this.anchorY + ", originZ=" + this.anchorZ + ", min=" + String.valueOf(this.min) + ", max=" + String.valueOf(this.max);
    }
    
    static {
        DEFAULT_ENTITY_CONSUMER = (ref -> {});
        METRICS_REGISTRY = new MetricsRegistry<BlockSelection>().register("BlocksLock", selection -> selection.blocksLock.toString(), (Codec<String>)Codec.STRING).register("EntitiesLock", selection -> selection.entitiesLock.toString(), (Codec<String>)Codec.STRING).register("Position", selection -> new Vector3i(selection.x, selection.y, selection.z), (Codec<Vector3i>)Vector3i.CODEC).register("Anchor", selection -> new Vector3i(selection.anchorX, selection.anchorY, selection.anchorZ), (Codec<Vector3i>)Vector3i.CODEC).register("Min", BlockSelection::getSelectionMin, Vector3i.CODEC).register("Max", BlockSelection::getSelectionMax, Vector3i.CODEC).register("BlockCount", BlockSelection::getBlockCount, Codec.INTEGER).register("EntityCount", BlockSelection::getEntityCount, Codec.INTEGER);
        LOGGER = HytaleLogger.forEnclosingClass();
    }
    
    public enum FallbackMode
    {
        PASS_THOUGH, 
        COPY;
    }
    
    record BlockHolder(int blockId, int rotation, int filler, int supportValue, Holder<ChunkStore> holder) {
        @Nonnull
        public BlockHolder cloneBlockHolder() {
            if (this.holder == null) {
                return this;
            }
            return new BlockHolder(this.blockId, this.rotation, this.filler, this.supportValue, this.holder.clone());
        }
    }
    
    record FluidHolder(int fluidId, byte fluidLevel) {}
    
    @FunctionalInterface
    public interface BlockComparingIterator
    {
        boolean test(final int p0, final int p1, final int p2, final BlockHolder p3);
    }
    
    @FunctionalInterface
    public interface BlockIterator
    {
        void accept(final int p0, final int p1, final int p2, final BlockHolder p3);
    }
    
    @FunctionalInterface
    public interface FluidIterator
    {
        void accept(final int p0, final int p1, final int p2, final int p3, final byte p4);
    }
}
