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

package com.hypixel.hytale.builtin.buildertools;

import com.hypixel.hytale.server.core.universe.world.chunk.section.FluidSection;
import com.hypixel.hytale.server.core.universe.world.chunk.AbstractCachedAccessor;
import com.hypixel.hytale.codec.validation.Validator;
import com.hypixel.hytale.codec.validation.Validators;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.server.core.prefab.PrefabLoadException;
import com.hypixel.hytale.server.core.prefab.PrefabSaveException;
import com.hypixel.hytale.server.core.modules.singleplayer.SingleplayerModule;
import com.hypixel.hytale.common.util.PathUtil;
import com.hypixel.hytale.server.core.prefab.PrefabStore;
import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection;
import com.hypixel.hytale.protocol.packets.interface_.EditorBlocksChange;
import com.hypixel.hytale.builtin.buildertools.snapshot.EntityTransformSnapshot;
import com.hypixel.hytale.server.core.util.TargetUtil;
import it.unimi.dsi.fastutil.ints.Int2IntFunction;
import java.util.function.IntPredicate;
import com.hypixel.hytale.assetstore.map.IndexedLookupTableAssetMap;
import com.hypixel.hytale.builtin.buildertools.utils.Material;
import com.hypixel.hytale.math.block.BlockCubeUtil;
import com.hypixel.hytale.math.Axis;
import com.hypixel.hytale.builtin.buildertools.snapshot.EntityAddSnapshot;
import com.hypixel.hytale.server.core.util.PrefabUtil;
import com.hypixel.hytale.server.core.util.FillerBlockUtil;
import com.hypixel.hytale.server.core.asset.type.blockhitbox.BlockBoundingBoxes;
import com.hypixel.hytale.math.vector.Vector4d;
import com.hypixel.hytale.protocol.packets.interface_.BlockChange;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.Rotation;
import com.hypixel.hytale.math.vector.Vector3f;
import com.hypixel.hytale.math.matrix.Matrix4d;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple;
import java.util.Set;
import com.hypixel.hytale.protocol.Packet;
import java.util.function.Supplier;
import java.util.Objects;
import com.hypixel.hytale.component.RemoveReason;
import com.hypixel.hytale.builtin.buildertools.snapshot.EntityRemoveSnapshot;
import java.util.ArrayList;
import com.hypixel.hytale.server.core.blocktype.component.BlockPhysics;
import com.hypixel.hytale.server.core.universe.world.chunk.ChunkColumn;
import java.util.HashSet;
import com.hypixel.hytale.builtin.buildertools.snapshot.ClipboardContentsSnapshot;
import com.hypixel.hytale.function.predicate.TriIntObjPredicate;
import com.hypixel.hytale.math.block.BlockSphereUtil;
import java.util.concurrent.atomic.AtomicInteger;
import com.hypixel.hytale.builtin.buildertools.snapshot.ClipboardBoundsSnapshot;
import com.hypixel.hytale.server.core.universe.world.accessor.BlockAccessor;
import java.util.function.Predicate;
import com.hypixel.hytale.math.iterator.LineIterator;
import com.hypixel.hytale.server.core.universe.world.accessor.LocalCachedChunkAccessor;
import com.hypixel.hytale.protocol.packets.buildertools.BrushOrigin;
import com.hypixel.hytale.protocol.packets.buildertools.BrushShape;
import com.hypixel.hytale.protocol.BlockMaterial;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntMaps;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import com.hypixel.hytale.assetstore.map.BlockTypeAssetMap;
import com.hypixel.hytale.protocol.DrawType;
import com.hypixel.hytale.server.core.universe.world.accessor.OverridableChunkAccessor;
import com.hypixel.hytale.math.block.BlockUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import com.hypixel.hytale.server.core.prefab.selection.mask.BlockPattern;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.BrushConfigEditStore;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.BrushConfigCommandExecutor;
import com.hypixel.hytale.server.core.command.system.CommandManager;
import com.hypixel.hytale.builtin.buildertools.tooloperations.PaintOperation;
import com.hypixel.hytale.builtin.buildertools.tooloperations.ToolOperation;
import com.hypixel.hytale.server.core.entity.UUIDComponent;
import com.hypixel.hytale.protocol.packets.buildertools.BuilderToolOnUseInteraction;
import com.hypixel.hytale.server.core.universe.world.SoundUtil;
import com.hypixel.hytale.protocol.SoundCategory;
import com.hypixel.hytale.server.core.util.TempAssetIdUtil;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.builtin.buildertools.snapshot.BlockSelectionSnapshot;
import com.hypixel.hytale.common.util.CompletableFutureUtil;
import java.nio.file.Path;
import java.util.Random;
import com.hypixel.hytale.server.core.prefab.selection.mask.BlockMask;
import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue;
import java.util.concurrent.locks.StampedLock;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.Collections;
import com.hypixel.hytale.builtin.buildertools.snapshot.SelectionSnapshot;
import com.hypixel.hytale.component.system.EcsEvent;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.system.WorldEventSystem;
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
import com.hypixel.hytale.server.core.entity.entities.player.pages.CustomUIPage;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.server.core.prefab.event.PrefabPasteEvent;
import com.hypixel.hytale.server.core.universe.world.accessor.ChunkAccessor;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.math.vector.VectorBoxUtil;
import com.hypixel.hytale.component.Archetype;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import com.hypixel.hytale.server.core.prefab.PrefabCopyableComponent;
import java.util.concurrent.Executor;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import com.hypixel.hytale.server.core.universe.world.meta.BlockState;
import com.hypixel.hytale.server.core.universe.world.meta.BlockStateModule;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.component.Holder;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.metrics.MetricResults;
import com.hypixel.hytale.sneakythrow.consumer.ThrowableConsumer;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.server.core.util.NotificationUtil;
import com.hypixel.hytale.protocol.packets.interface_.NotificationStyle;
import com.hypixel.hytale.server.core.command.system.CommandSender;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.BuilderTool;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.BuilderToolData;
import com.hypixel.hytale.server.core.asset.type.item.config.Item;
import com.hypixel.hytale.server.core.inventory.ItemStack;
import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.args.ToolArgException;
import com.hypixel.hytale.server.core.util.MessageUtil;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.protocol.packets.buildertools.BuilderToolArgUpdate;
import com.hypixel.hytale.server.core.HytaleServer;
import java.util.concurrent.TimeUnit;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.BreakpointOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.ReplaceOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.EchoOnceOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.EchoOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.PastePrefabOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.saveandload.PersistentDataOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.ErodeOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.offsets.RandomOffsetOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.global.DisableHoldInteractionOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.DeleteOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.MaterialOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.MeltOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.HeightmapLayerOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.LayerOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.offsets.OffsetOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.ShapeOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.SmoothOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.SetOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.SetDensity;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.LiftOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.LoadIntFromToolArgOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.LoadMaterialFromToolArgOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.BlockPatternOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.JumpIfToolArgOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.JumpIfBlockTypeOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.JumpIfCompareOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.JumpIfClickType;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.JumpIfStringMatchOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.JumpToRandomIndex;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.ExitOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.JumpToIndexOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.saveandload.LoadOperationsFromAssetOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.saveandload.SaveIndexOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.saveandload.LoadBrushConfigOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.saveandload.SaveBrushConfigOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.loops.CircleOffsetFromArgOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.loops.CircleOffsetAndLoopOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.loops.LoopRandomOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.loops.LoadLoopFromToolArgOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.flowcontrol.loops.LoopOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.global.DebugBrushOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.global.IgnoreExistingBrushDataOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.masks.AppendMaskFromToolArgOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.masks.AppendMaskOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.masks.UseOperationMaskOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.masks.UseBrushMaskOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.ClearOperationMaskOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.masks.MaskOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.masks.HistoryMaskOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.RunCommandOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.dimensions.RandomizeDimensionsOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.sequential.dimensions.DimensionsOperation;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.operations.system.BrushOperation;
import com.hypixel.hytale.component.ComponentRegistryProxy;
import com.hypixel.hytale.event.EventRegistry;
import com.hypixel.hytale.server.core.command.system.CommandRegistry;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.args.OptionArg;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.args.BrushOriginArg;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.args.BrushShapeArg;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.args.MaskArg;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.args.BlockArg;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.args.FloatArg;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.args.IntArg;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.args.StringArg;
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.args.BoolArg;
import com.hypixel.hytale.server.core.asset.type.buildertool.config.args.ToolArg;
import com.hypixel.hytale.server.core.prefab.selection.SelectionManager;
import com.hypixel.hytale.server.core.plugin.PluginBase;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.server.OpenCustomUIInteraction;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.server.core.modules.prefabspawner.PrefabSpawnerState;
import com.hypixel.hytale.builtin.buildertools.imageimport.ImageImportCommand;
import com.hypixel.hytale.builtin.buildertools.objimport.ObjImportCommand;
import com.hypixel.hytale.builtin.buildertools.commands.SetToolHistorySizeCommand;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.commands.BrushConfigCommand;
import com.hypixel.hytale.builtin.buildertools.commands.FillCommand;
import com.hypixel.hytale.builtin.buildertools.commands.HollowCommand;
import com.hypixel.hytale.builtin.buildertools.commands.WallsCommand;
import com.hypixel.hytale.builtin.buildertools.commands.HotbarSwitchCommand;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.commands.PrefabEditCommand;
import com.hypixel.hytale.builtin.buildertools.commands.RepairFillersCommand;
import com.hypixel.hytale.builtin.buildertools.commands.GlobalMaskCommand;
import com.hypixel.hytale.builtin.buildertools.commands.UpdateSelectionCommand;
import com.hypixel.hytale.builtin.buildertools.commands.UndoCommand;
import com.hypixel.hytale.builtin.buildertools.commands.TintCommand;
import com.hypixel.hytale.builtin.buildertools.commands.SubmergeCommand;
import com.hypixel.hytale.builtin.buildertools.commands.StackCommand;
import com.hypixel.hytale.builtin.buildertools.commands.ShiftCommand;
import com.hypixel.hytale.builtin.buildertools.commands.SetCommand;
import com.hypixel.hytale.builtin.buildertools.commands.SelectionHistoryCommand;
import com.hypixel.hytale.builtin.buildertools.commands.SelectChunkSectionCommand;
import com.hypixel.hytale.builtin.buildertools.commands.SelectChunkCommand;
import com.hypixel.hytale.builtin.buildertools.commands.RotateCommand;
import com.hypixel.hytale.builtin.buildertools.commands.ReplaceCommand;
import com.hypixel.hytale.builtin.buildertools.commands.RedoCommand;
import com.hypixel.hytale.builtin.buildertools.commands.PrefabCommand;
import com.hypixel.hytale.builtin.buildertools.commands.Pos2Command;
import com.hypixel.hytale.builtin.buildertools.commands.Pos1Command;
import com.hypixel.hytale.builtin.buildertools.commands.PasteCommand;
import com.hypixel.hytale.builtin.buildertools.commands.MoveCommand;
import com.hypixel.hytale.builtin.buildertools.commands.FlipCommand;
import com.hypixel.hytale.builtin.buildertools.commands.ExtendFaceCommand;
import com.hypixel.hytale.builtin.buildertools.commands.ExpandCommand;
import com.hypixel.hytale.builtin.buildertools.commands.EnvironmentCommand;
import com.hypixel.hytale.builtin.buildertools.commands.EditLineCommand;
import com.hypixel.hytale.builtin.buildertools.commands.CutCommand;
import com.hypixel.hytale.builtin.buildertools.commands.DeselectCommand;
import com.hypixel.hytale.builtin.buildertools.commands.CopyCommand;
import com.hypixel.hytale.builtin.buildertools.commands.ContractSelectionCommand;
import com.hypixel.hytale.builtin.buildertools.commands.ClearEditHistory;
import com.hypixel.hytale.builtin.buildertools.commands.ClearEntitiesCommand;
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
import com.hypixel.hytale.builtin.buildertools.commands.ClearBlocksCommand;
import com.hypixel.hytale.builtin.buildertools.scriptedbrushes.ScriptedBrushAsset;
import com.hypixel.hytale.assetstore.AssetRegistry;
import com.hypixel.hytale.assetstore.codec.AssetCodec;
import com.hypixel.hytale.assetstore.map.DefaultAssetMap;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.PrefabEditorCreationSettings;
import com.hypixel.hytale.server.core.asset.HytaleAssetStore;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.RootInteraction;
import java.util.List;
import com.hypixel.hytale.builtin.buildertools.interactions.PickupItemInteraction;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.PrefabSetAnchorInteraction;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.PrefabSelectionInteraction;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.PrefabDirtySystems;
import com.hypixel.hytale.component.system.ISystem;
import com.hypixel.hytale.server.core.universe.world.worldmap.WorldMapManager;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.PrefabMarkerProvider;
import com.hypixel.hytale.server.core.universe.world.events.AddWorldEvent;
import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent;
import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent;
import com.hypixel.hytale.server.core.io.handlers.SubPacketHandler;
import com.hypixel.hytale.server.core.io.handlers.IPacketHandler;
import java.util.function.Function;
import com.hypixel.hytale.server.core.io.ServerManager;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.sneakythrow.consumer.ThrowableTriConsumer;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import java.util.Iterator;
import it.unimi.dsi.fastutil.longs.LongSet;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.math.util.ChunkUtil;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import com.hypixel.hytale.math.vector.Vector3i;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.prefab.selection.standard.BlockSelection;
import java.util.logging.Level;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.PrefabEditSession;
import com.hypixel.hytale.component.ResourceType;
import com.hypixel.hytale.server.core.util.Config;
import java.util.concurrent.ConcurrentHashMap;
import com.hypixel.fastutil.ints.Int2ObjectConcurrentHashMap;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.PrefabAnchor;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.ComponentType;
import javax.annotation.Nullable;
import java.util.concurrent.ScheduledFuture;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.PrefabEditSessionManager;
import java.util.UUID;
import java.util.Map;
import com.hypixel.hytale.metrics.MetricsRegistry;
import com.hypixel.hytale.server.core.prefab.selection.standard.FeedbackConsumer;
import com.hypixel.hytale.metrics.MetricProvider;
import com.hypixel.hytale.server.core.prefab.selection.SelectionProvider;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;

public class BuilderToolsPlugin extends JavaPlugin implements SelectionProvider, MetricProvider
{
    public static final String EDITOR_BLOCK = "Editor_Block";
    public static final String EDITOR_BLOCK_PREFAB_AIR = "Editor_Empty";
    public static final String EDITOR_BLOCK_PREFAB_ANCHOR = "Editor_Anchor";
    protected static final float SPHERE_SIZE = 1.0f;
    private static final FeedbackConsumer FEEDBACK_CONSUMER;
    private static final MetricsRegistry<BuilderToolsPlugin> PLUGIN_METRICS_REGISTRY;
    private static final long RETAIN_BUILDER_STATE_TIMESTAMP = Long.MAX_VALUE;
    private static final long MIN_CLEANUP_INTERVAL_NANOS;
    private final Map<UUID, BuilderState> builderStates;
    private PrefabEditSessionManager prefabEditSessionManager;
    private final BlockColorIndex blockColorIndex;
    private static BuilderToolsPlugin instance;
    private int historyCount;
    private long toolExpireTimeNanos;
    @Nullable
    private ScheduledFuture<?> cleanupTask;
    private ComponentType<EntityStore, BuilderToolsUserData> userDataComponentType;
    private ComponentType<EntityStore, PrefabAnchor> prefabAnchorComponentType;
    private final Int2ObjectConcurrentHashMap<ConcurrentHashMap<UUID, UUID>> pastedPrefabPathUUIDMap;
    private final Int2ObjectConcurrentHashMap<ConcurrentHashMap<String, UUID>> pastedPrefabPathNameToUUIDMap;
    private static final float SMOOTHING_KERNEL_TOTAL = 27.0f;
    private static final int[] SMOOTHING_KERNEL;
    private final Config<BuilderToolsConfig> config;
    private ResourceType<EntityStore, PrefabEditSession> prefabEditSessionResourceType;
    
    public BuilderToolsPlugin(@Nonnull final JavaPluginInit init) {
        super(init);
        this.builderStates = new ConcurrentHashMap<UUID, BuilderState>();
        this.blockColorIndex = new BlockColorIndex();
        this.pastedPrefabPathUUIDMap = new Int2ObjectConcurrentHashMap<ConcurrentHashMap<UUID, UUID>>();
        this.pastedPrefabPathNameToUUIDMap = new Int2ObjectConcurrentHashMap<ConcurrentHashMap<String, UUID>>();
        this.config = this.withConfig("BuilderToolsModule", BuilderToolsConfig.CODEC);
        BuilderToolsPlugin.instance = this;
        this.getLogger().setLevel(Level.FINE);
    }
    
    public static BuilderToolsPlugin get() {
        return BuilderToolsPlugin.instance;
    }
    
    @Nonnull
    public BlockColorIndex getBlockColorIndex() {
        return this.blockColorIndex;
    }
    
    public static void invalidateWorldMapForSelection(@Nonnull final BlockSelection selection, @Nonnull final World world) {
        invalidateWorldMapForBounds(selection.getSelectionMin(), selection.getSelectionMax(), world);
    }
    
    static void invalidateWorldMapForBounds(@Nonnull final Vector3i min, @Nonnull final Vector3i max, @Nonnull final World world) {
        final LongSet affectedChunks = new LongOpenHashSet();
        final int minChunkX = min.x >> 5;
        final int maxChunkX = max.x >> 5;
        final int minChunkZ = min.z >> 5;
        final int maxChunkZ = max.z >> 5;
        for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
            for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
                affectedChunks.add(ChunkUtil.indexChunk(cx, cz));
            }
        }
        world.getWorldMapManager().clearImagesInChunks(affectedChunks);
        for (final Player worldPlayer : world.getPlayers()) {
            worldPlayer.getWorldMapTracker().clearChunks(affectedChunks);
        }
    }
    
    @Nonnull
    public static BuilderState getState(@Nonnull final Player player, @Nonnull final PlayerRef playerRef) {
        return BuilderToolsPlugin.instance.getBuilderState(player, playerRef);
    }
    
    public static <T extends Throwable> void addToQueue(@Nonnull final Player player, @Nonnull final PlayerRef playerRef, @Nonnull final ThrowableTriConsumer<Ref<EntityStore>, BuilderState, ComponentAccessor<EntityStore>, T> task) {
        getState(player, playerRef).addToQueue(task);
    }
    
    @Override
    protected void setup() {
        final CommandRegistry commandRegistry = this.getCommandRegistry();
        final EventRegistry eventRegistry = this.getEventRegistry();
        final ComponentRegistryProxy<EntityStore> entityStoreRegistry = this.getEntityStoreRegistry();
        ServerManager.get().registerSubPacketHandlers((Function<IPacketHandler, SubPacketHandler>)BuilderToolsPacketHandler::new);
        eventRegistry.register(PlayerConnectEvent.class, this::onPlayerConnect);
        eventRegistry.register(PlayerDisconnectEvent.class, this::onPlayerDisconnect);
        eventRegistry.registerGlobal(AddWorldEvent.class, event -> event.getWorld().getWorldMapManager().addMarkerProvider("prefabs", PrefabMarkerProvider.INSTANCE));
        entityStoreRegistry.registerSystem(new PrefabPasteEventSystem(this));
        entityStoreRegistry.registerSystem(new PrefabDirtySystems.BlockBreakDirtySystem());
        entityStoreRegistry.registerSystem(new PrefabDirtySystems.BlockPlaceDirtySystem());
        this.prefabAnchorComponentType = entityStoreRegistry.registerComponent(PrefabAnchor.class, "PrefabAnchor", PrefabAnchor.CODEC);
        Interaction.CODEC.register("PrefabSelectionInteraction", PrefabSelectionInteraction.class, PrefabSelectionInteraction.CODEC);
        Interaction.CODEC.register("PrefabSetAnchorInteraction", PrefabSetAnchorInteraction.class, PrefabSetAnchorInteraction.CODEC);
        Interaction.CODEC.register("PickupItem", PickupItemInteraction.class, PickupItemInteraction.CODEC);
        Interaction.getAssetStore().loadAssets("Hytale:Hytale", List.of(new PickupItemInteraction("*PickupItem")));
        RootInteraction.getAssetStore().loadAssets("Hytale:Hytale", List.of(PickupItemInteraction.DEFAULT_ROOT));
        this.prefabEditSessionManager = new PrefabEditSessionManager(this);
        this.prefabEditSessionResourceType = entityStoreRegistry.registerResource(PrefabEditSession.class, "PrefabEditSession", PrefabEditSession.CODEC);
        AssetRegistry.register(((HytaleAssetStore.Builder)((HytaleAssetStore.Builder)((HytaleAssetStore.Builder)HytaleAssetStore.builder(PrefabEditorCreationSettings.class, new DefaultAssetMap()).setPath()).setKeyFunction(PrefabEditorCreationSettings::getId)).setCodec(PrefabEditorCreationSettings.CODEC)).build());
        AssetRegistry.register(((HytaleAssetStore.Builder)((HytaleAssetStore.Builder)((HytaleAssetStore.Builder)HytaleAssetStore.builder(ScriptedBrushAsset.class, new DefaultAssetMap()).setPath()).setKeyFunction(ScriptedBrushAsset::getId)).setCodec(ScriptedBrushAsset.CODEC)).build());
        commandRegistry.registerCommand(new ClearBlocksCommand());
        commandRegistry.registerCommand(new ClearEntitiesCommand());
        commandRegistry.registerCommand(new ClearEditHistory());
        commandRegistry.registerCommand(new ContractSelectionCommand());
        commandRegistry.registerCommand(new CopyCommand());
        commandRegistry.registerCommand(new DeselectCommand());
        commandRegistry.registerCommand(new CutCommand());
        commandRegistry.registerCommand(new EditLineCommand());
        commandRegistry.registerCommand(new EnvironmentCommand());
        commandRegistry.registerCommand(new ExpandCommand());
        commandRegistry.registerCommand(new ExtendFaceCommand());
        commandRegistry.registerCommand(new FlipCommand());
        commandRegistry.registerCommand(new MoveCommand());
        commandRegistry.registerCommand(new PasteCommand());
        commandRegistry.registerCommand(new Pos1Command());
        commandRegistry.registerCommand(new Pos2Command());
        commandRegistry.registerCommand(new PrefabCommand());
        commandRegistry.registerCommand(new RedoCommand());
        commandRegistry.registerCommand(new ReplaceCommand());
        commandRegistry.registerCommand(new RotateCommand());
        commandRegistry.registerCommand(new SelectChunkCommand());
        commandRegistry.registerCommand(new SelectChunkSectionCommand());
        commandRegistry.registerCommand(new SelectionHistoryCommand());
        commandRegistry.registerCommand(new SetCommand());
        commandRegistry.registerCommand(new ShiftCommand());
        commandRegistry.registerCommand(new StackCommand());
        commandRegistry.registerCommand(new SubmergeCommand());
        commandRegistry.registerCommand(new TintCommand());
        commandRegistry.registerCommand(new UndoCommand());
        commandRegistry.registerCommand(new UpdateSelectionCommand());
        commandRegistry.registerCommand(new GlobalMaskCommand());
        commandRegistry.registerCommand(new RepairFillersCommand());
        commandRegistry.registerCommand(new PrefabEditCommand());
        commandRegistry.registerCommand(new HotbarSwitchCommand());
        commandRegistry.registerCommand(new WallsCommand());
        commandRegistry.registerCommand(new HollowCommand());
        commandRegistry.registerCommand(new FillCommand());
        commandRegistry.registerCommand(new BrushConfigCommand());
        commandRegistry.registerCommand(new SetToolHistorySizeCommand());
        commandRegistry.registerCommand(new ObjImportCommand());
        commandRegistry.registerCommand(new ImageImportCommand());
        OpenCustomUIInteraction.registerBlockCustomPage(this, PrefabSpawnerState.PrefabSpawnerSettingsPage.class, "PrefabSpawner", PrefabSpawnerState.class, (playerRef, state) -> new PrefabSpawnerState.PrefabSpawnerSettingsPage(playerRef, state, CustomPageLifetime.CanDismissOrCloseThroughInteraction));
        SelectionManager.setSelectionProvider(this);
        ToolArg.CODEC.register("Bool", BoolArg.class, BoolArg.CODEC);
        ToolArg.CODEC.register("String", StringArg.class, StringArg.CODEC);
        ToolArg.CODEC.register("Int", IntArg.class, IntArg.CODEC);
        ToolArg.CODEC.register("Float", FloatArg.class, FloatArg.CODEC);
        ToolArg.CODEC.register("Block", BlockArg.class, BlockArg.CODEC);
        ToolArg.CODEC.register("Mask", MaskArg.class, MaskArg.CODEC);
        ToolArg.CODEC.register("BrushShape", BrushShapeArg.class, BrushShapeArg.CODEC);
        ToolArg.CODEC.register("BrushOrigin", BrushOriginArg.class, BrushOriginArg.CODEC);
        ToolArg.CODEC.register("Option", OptionArg.class, OptionArg.CODEC);
        this.registerBrushOperations();
        this.userDataComponentType = entityStoreRegistry.registerComponent(BuilderToolsUserData.class, "BuilderTools", BuilderToolsUserData.CODEC);
        entityStoreRegistry.registerSystem(new BuilderToolsSystems.EnsureBuilderTools());
        entityStoreRegistry.registerSystem(new BuilderToolsUserDataSystem());
    }
    
    private void registerBrushOperations() {
        BrushOperation.OPERATION_CODEC.register("dimensions", DimensionsOperation.class, DimensionsOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("randomdimensions", RandomizeDimensionsOperation.class, RandomizeDimensionsOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("runcommand", RunCommandOperation.class, RunCommandOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("historymask", HistoryMaskOperation.class, HistoryMaskOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("mask", MaskOperation.class, MaskOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("clearoperationmask", ClearOperationMaskOperation.class, ClearOperationMaskOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("usebrushmask", UseBrushMaskOperation.class, UseBrushMaskOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("useoperationmask", UseOperationMaskOperation.class, UseOperationMaskOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("appendmask", AppendMaskOperation.class, AppendMaskOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("appendmaskfromtoolarg", AppendMaskFromToolArgOperation.class, AppendMaskFromToolArgOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("ignorebrushsettings", IgnoreExistingBrushDataOperation.class, IgnoreExistingBrushDataOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("debug", DebugBrushOperation.class, DebugBrushOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("loop", LoopOperation.class, LoopOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("loadloop", LoadLoopFromToolArgOperation.class, LoadLoopFromToolArgOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("looprandom", LoopRandomOperation.class, LoopRandomOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("loopcircle", CircleOffsetAndLoopOperation.class, CircleOffsetAndLoopOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("loopcirclefromarg", CircleOffsetFromArgOperation.class, CircleOffsetFromArgOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("savebrushconfig", SaveBrushConfigOperation.class, SaveBrushConfigOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("loadbrushconfig", LoadBrushConfigOperation.class, LoadBrushConfigOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("saveindex", SaveIndexOperation.class, SaveIndexOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("loadoperationsfromasset", LoadOperationsFromAssetOperation.class, LoadOperationsFromAssetOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("jump", JumpToIndexOperation.class, JumpToIndexOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("exit", ExitOperation.class, ExitOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("jumprandom", JumpToRandomIndex.class, JumpToRandomIndex.CODEC);
        BrushOperation.OPERATION_CODEC.register("jumpifequal", JumpIfStringMatchOperation.class, JumpIfStringMatchOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("jumpifclicktype", JumpIfClickType.class, JumpIfClickType.CODEC);
        BrushOperation.OPERATION_CODEC.register("jumpifcompare", JumpIfCompareOperation.class, JumpIfCompareOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("jumpifblocktype", JumpIfBlockTypeOperation.class, JumpIfBlockTypeOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("jumpiftoolarg", JumpIfToolArgOperation.class, JumpIfToolArgOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("pattern", BlockPatternOperation.class, BlockPatternOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("loadmaterial", LoadMaterialFromToolArgOperation.class, LoadMaterialFromToolArgOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("loadint", LoadIntFromToolArgOperation.class, LoadIntFromToolArgOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("lift", LiftOperation.class, LiftOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("density", SetDensity.class, SetDensity.CODEC);
        BrushOperation.OPERATION_CODEC.register("set", SetOperation.class, SetOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("smooth", SmoothOperation.class, SmoothOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("shape", ShapeOperation.class, ShapeOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("offset", OffsetOperation.class, OffsetOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("layer", LayerOperation.class, LayerOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("heightmaplayer", HeightmapLayerOperation.class, HeightmapLayerOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("melt", MeltOperation.class, MeltOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("material", MaterialOperation.class, MaterialOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("delete", DeleteOperation.class, DeleteOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("disableonhold", DisableHoldInteractionOperation.class, DisableHoldInteractionOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("randomoffset", RandomOffsetOperation.class, RandomOffsetOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("erode", ErodeOperation.class, ErodeOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("persistentdata", PersistentDataOperation.class, PersistentDataOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("pasteprefab", PastePrefabOperation.class, PastePrefabOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("echo", EchoOperation.class, EchoOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("echoonce", EchoOnceOperation.class, EchoOnceOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("replace", ReplaceOperation.class, ReplaceOperation.CODEC);
        BrushOperation.OPERATION_CODEC.register("breakpoint", BreakpointOperation.class, BreakpointOperation.CODEC);
    }
    
    public ResourceType<EntityStore, PrefabEditSession> getPrefabEditSessionResourceType() {
        return this.prefabEditSessionResourceType;
    }
    
    @Override
    protected void start() {
        final BuilderToolsConfig config = this.config.get();
        this.historyCount = config.historyCount;
        this.toolExpireTimeNanos = TimeUnit.SECONDS.toNanos(config.toolExpireTime);
        if (this.toolExpireTimeNanos > 0L) {
            final long intervalNanos = Math.max(BuilderToolsPlugin.MIN_CLEANUP_INTERVAL_NANOS, this.toolExpireTimeNanos);
            this.cleanupTask = HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay(this::cleanup, intervalNanos, intervalNanos, TimeUnit.NANOSECONDS);
        }
    }
    
    @Override
    protected void shutdown() {
        if (this.cleanupTask != null) {
            this.cleanupTask.cancel(false);
        }
    }
    
    private void cleanup() {
        final long expire = System.nanoTime() - this.toolExpireTimeNanos;
        final Iterator<Map.Entry<UUID, BuilderState>> iterator = this.builderStates.entrySet().iterator();
        while (iterator.hasNext()) {
            final Map.Entry<UUID, BuilderState> entry = iterator.next();
            final BuilderState state = entry.getValue();
            if (state.timestamp < expire) {
                iterator.remove();
                this.getLogger().at(Level.FINE).log("[%s] Expired and removed builder tool", state.getDisplayName());
            }
        }
    }
    
    public void setToolHistorySize(final int size) {
        this.historyCount = size;
    }
    
    private void onPlayerConnect(@Nonnull final PlayerConnectEvent event) {
        this.retainBuilderState(event.getPlayer(), event.getPlayerRef());
    }
    
    private void onPlayerDisconnect(@Nonnull final PlayerDisconnectEvent event) {
        this.releaseBuilderState(event.getPlayerRef().getUuid());
    }
    
    public void onToolArgUpdate(@Nonnull final PlayerRef playerRef, @Nonnull final Player player, @Nonnull final BuilderToolArgUpdate packet) {
        final ItemContainer section = player.getInventory().getSectionById(packet.section);
        final ItemStack itemStack = section.getItemStack((short)packet.slot);
        if (itemStack == null) {
            MessageUtil.sendFailureReply(playerRef, packet.token, Message.translation("server.builderTools.invalidTool").param("item", "Empty"));
            return;
        }
        final Item item = itemStack.getItem();
        final BuilderToolData builderToolData = item.getBuilderToolData();
        if (builderToolData == null) {
            final Message itemMessage = Message.translation(item.getTranslationKey());
            MessageUtil.sendFailureReply(playerRef, packet.token, Message.translation("server.builderTools.invalidTool").param("item", itemMessage));
            return;
        }
        final BuilderTool tool = builderToolData.getTools()[0];
        try {
            final ItemStack updatedItemStack = tool.updateArgMetadata(itemStack, packet.group, packet.id, packet.value);
            section.setItemStackForSlot((short)packet.slot, updatedItemStack);
            MessageUtil.sendSuccessReply(playerRef, packet.token);
        }
        catch (final ToolArgException e) {
            MessageUtil.sendFailureReply(playerRef, packet.token, e.getTranslationMessage());
        }
        catch (final IllegalArgumentException e2) {
            MessageUtil.sendFailureReply(playerRef, packet.token, Message.translation("server.builderTools.toolArgParseError").param("arg", packet.id).param("value", packet.value));
        }
    }
    
    @Nonnull
    public BuilderState getBuilderState(@Nonnull final Player player, @Nonnull final PlayerRef playerRef) {
        return this.builderStates.computeIfAbsent(playerRef.getUuid(), k -> new BuilderState(player, playerRef));
    }
    
    @Nullable
    public BuilderState clearBuilderState(final UUID uuid) {
        final BuilderState state = this.builderStates.remove(uuid);
        if (state != null) {
            this.getLogger().at(Level.FINE).log("[%s] Removed builder tool for", state.getDisplayName());
        }
        return state;
    }
    
    private void retainBuilderState(@Nonnull final Player player, @Nonnull final PlayerRef playerRef) {
        this.builderStates.compute(playerRef.getUuid(), (id, state) -> {
            if (state == null) {
                return null;
            }
            else {
                state.retain(player, playerRef);
                this.getLogger().at(Level.FINE).log("[%s] Retained builder tool", state.getDisplayName());
                return (BuilderState)(BuilderState)state;
            }
        });
    }
    
    private void releaseBuilderState(@Nonnull final UUID uuid) {
        if (this.toolExpireTimeNanos <= 0L) {
            this.clearBuilderState(uuid);
            return;
        }
        this.builderStates.compute(uuid, (id, state) -> {
            if (state == null) {
                return null;
            }
            else {
                state.release();
                this.getLogger().at(Level.FINE).log("[%s] Marked builder tool for removal", state.getDisplayName());
                return (BuilderState)(BuilderState)state;
            }
        });
    }
    
    public ComponentType<EntityStore, BuilderToolsUserData> getUserDataComponentType() {
        return this.userDataComponentType;
    }
    
    public static void sendFeedback(@Nonnull final Message message, @Nullable final CommandSender feedback, @Nonnull final NotificationStyle notificationStyle, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        if (feedback instanceof final Player playerComponent) {
            final Ref<EntityStore> ref = playerComponent.getReference();
            if (ref == null || !ref.isValid()) {
                return;
            }
            final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
            assert playerRefComponent != null;
            NotificationUtil.sendNotification(playerRefComponent.getPacketHandler(), message, notificationStyle);
        }
        else if (feedback != null) {
            feedback.sendMessage(message);
        }
    }
    
    public static void sendFeedback(@Nonnull final String key, final int total, final CommandSender feedback, final ComponentAccessor<EntityStore> componentAccessor) {
        if (feedback instanceof final Player playerComponent) {
            final Ref<EntityStore> ref = playerComponent.getReference();
            if (ref == null || !ref.isValid()) {
                return;
            }
            final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
            assert playerRefComponent != null;
            NotificationUtil.sendNotification(playerRefComponent.getPacketHandler(), Message.translation("server.builderTools.blocksEdited").param("key", key), Message.raw(String.valueOf(total)), NotificationStyle.Success);
        }
        else if (feedback != null) {
            feedback.sendMessage(Message.translation("server.builderTools.blocksEdited").param("key", key));
        }
    }
    
    public static void sendFeedback(@Nonnull final String key, final int total, final int num, final CommandSender feedback, final ComponentAccessor<EntityStore> componentAccessor) {
        if (num % 100000 == 0) {
            if (feedback instanceof final Player playerComponent) {
                final Ref<EntityStore> ref = playerComponent.getReference();
                if (ref == null || !ref.isValid()) {
                    return;
                }
                final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
                assert playerRefComponent != null;
                NotificationUtil.sendNotification(playerRefComponent.getPacketHandler(), Message.translation("server.builderTools.doneEditing").param("key", key), Message.translation("server.builderTools.blocksChanged").param("total", total), NotificationStyle.Success);
            }
            else if (feedback != null) {
                feedback.sendMessage(Message.translation("server.builderTools.editingStatus").param("key", key).param("percent", MathUtil.round(num / (double)total * 100.0, 2)).param("count", num).param("total", total));
            }
        }
    }
    
    @Override
    public <T extends Throwable> void computeSelectionCopy(@Nonnull final Ref<EntityStore> ref, @Nonnull final Player player, @Nonnull final ThrowableConsumer<BlockSelection, T> task, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        if (this.isEnabled()) {
            final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
            assert playerRefComponent != null;
            this.getBuilderState(player, playerRefComponent).computeSelectionCopy(task);
        }
    }
    
    @Nonnull
    @Override
    public MetricResults toMetricResults() {
        return BuilderToolsPlugin.PLUGIN_METRICS_REGISTRY.toMetricResults(this);
    }
    
    public ComponentType<EntityStore, PrefabAnchor> getPrefabAnchorComponentType() {
        return this.prefabAnchorComponentType;
    }
    
    public PrefabEditSessionManager getPrefabEditSessionManager() {
        return this.prefabEditSessionManager;
    }
    
    @Nullable
    @Deprecated
    public static Holder<ChunkStore> createBlockComponent(final WorldChunk chunk, final int x, final int y, final int z, final int newId, final int oldId, @Nullable final Holder<ChunkStore> oldHolder, final boolean copy) {
        if (newId == 0) {
            return null;
        }
        final BlockType type = BlockType.getAssetMap().getAsset(newId);
        if (type.getBlockEntity() != null) {
            return type.getBlockEntity().clone();
        }
        final String stateId = (type.getState() != null) ? type.getState().getId() : null;
        if (stateId == null) {
            return null;
        }
        if (copy && oldHolder != null) {
            final BlockType currentType = BlockType.getAssetMap().getAsset(oldId);
            final String currentStateId = (currentType.getState() != null) ? currentType.getState().getId() : null;
            if (stateId.equals(currentStateId)) {
                return oldHolder.clone();
            }
        }
        final Vector3i position = new Vector3i(x, y, z);
        final BlockState state = BlockStateModule.get().createBlockState(stateId, chunk, position, type);
        if (state == null) {
            return null;
        }
        return state.toHolder();
    }
    
    public static void forEachCopyableInSelection(@Nonnull final World world, final int minX, final int minY, final int minZ, final int width, final int height, final int depth, @Nonnull final Consumer<Ref<EntityStore>> action) {
        final int encompassingWidth = width + 1;
        final int encompassingHeight = height + 1;
        final int encompassingDepth = depth + 1;
        if (world.isInThread()) {
            internalForEachCopyableInSelection(world, minX, minY, minZ, encompassingWidth, encompassingHeight, encompassingDepth, action);
        }
        else {
            CompletableFuture.runAsync(() -> internalForEachCopyableInSelection(world, minX, minY, minZ, encompassingWidth, encompassingHeight, encompassingDepth, action), world).join();
        }
    }
    
    private static void internalForEachCopyableInSelection(@Nonnull final World world, final int minX, final int minY, final int minZ, final int encompassingWidth, final int encompassingHeight, final int encompassingDepth, @Nonnull final Consumer<Ref<EntityStore>> action) {
        world.getEntityStore().getStore().forEachChunk((Query<EntityStore>)Archetype.of(PrefabCopyableComponent.getComponentType(), TransformComponent.getComponentType()), (archetypeChunk, commandBuffer) -> {
            for (int size = archetypeChunk.size(), index = 0; index < size; ++index) {
                final Vector3d vector = archetypeChunk.getComponent(index, TransformComponent.getComponentType()).getPosition();
                final Ref<EntityStore> ref = archetypeChunk.getReferenceTo(index);
                if (VectorBoxUtil.isInside(minX, minY, minZ, 0.0, 0.0, 0.0, encompassingWidth, encompassingHeight, encompassingDepth, vector)) {
                    action.accept(ref);
                }
            }
        });
    }
    
    private static int getNonEmptyNeighbourBlock(@Nonnull final ChunkAccessor accessor, final int x, final int y, final int z) {
        int blockId;
        if ((blockId = accessor.getBlock(x, y, z + 1)) > 0) {
            return blockId;
        }
        if ((blockId = accessor.getBlock(x, y, z - 1)) > 0) {
            return blockId;
        }
        if ((blockId = accessor.getBlock(x, y + 1, z)) > 0) {
            return blockId;
        }
        if ((blockId = accessor.getBlock(x, y - 1, z)) > 0) {
            return blockId;
        }
        if ((blockId = accessor.getBlock(x - 1, y, z)) > 0) {
            return blockId;
        }
        if ((blockId = accessor.getBlock(x + 1, y, z)) > 0) {
            return blockId;
        }
        return 0;
    }
    
    @Nonnull
    public UUID getNewPathIdOnPrefabPasted(@Nullable final UUID id, final String name, final int prefabId) {
        final ConcurrentHashMap<UUID, UUID> prefabIdMap = this.pastedPrefabPathUUIDMap.get(prefabId);
        if (id != null) {
            return prefabIdMap.computeIfAbsent(id, k -> UUID.randomUUID());
        }
        final ConcurrentHashMap<String, UUID> prefabNameMap = this.pastedPrefabPathNameToUUIDMap.get(prefabId);
        final UUID newId = prefabNameMap.computeIfAbsent(name, k -> UUID.randomUUID());
        prefabIdMap.put(newId, newId);
        return newId;
    }
    
    public static boolean onPasteStart(final int prefabId, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final PrefabPasteEvent event = new PrefabPasteEvent(prefabId, true);
        componentAccessor.invoke(event);
        return !event.isCancelled();
    }
    
    public void onPasteEnd(final int prefabId, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final PrefabPasteEvent event = new PrefabPasteEvent(prefabId, false);
        componentAccessor.invoke(event);
    }
    
    public Int2ObjectConcurrentHashMap<ConcurrentHashMap<UUID, UUID>> getPastedPrefabPathUUIDMap() {
        return this.pastedPrefabPathUUIDMap;
    }
    
    static {
        FEEDBACK_CONSUMER = BuilderToolsPlugin::sendFeedback;
        PLUGIN_METRICS_REGISTRY = new MetricsRegistry<BuilderToolsPlugin>().register("BuilderStates", plugin -> plugin.builderStates.values().toArray(BuilderState[]::new), (Codec<BuilderState[]>)new ArrayCodec<BuilderState>(BuilderState.STATE_METRICS_REGISTRY, BuilderState[]::new));
        MIN_CLEANUP_INTERVAL_NANOS = TimeUnit.MINUTES.toNanos(1L);
        SMOOTHING_KERNEL = new int[] { 1, 2, 1, 2, 3, 2, 1, 2, 1, 2, 3, 2, 3, 4, 3, 2, 3, 2, 1, 2, 1, 2, 3, 2, 1, 2, 1 };
    }
    
    public static class PrefabPasteEventSystem extends WorldEventSystem<EntityStore, PrefabPasteEvent>
    {
        @Nonnull
        private final BuilderToolsPlugin plugin;
        
        protected PrefabPasteEventSystem(@Nonnull final BuilderToolsPlugin plugin) {
            super(PrefabPasteEvent.class);
            this.plugin = plugin;
        }
        
        @Override
        public void handle(@Nonnull final Store<EntityStore> store, @Nonnull final CommandBuffer<EntityStore> commandBuffer, @Nonnull final PrefabPasteEvent event) {
            if (event.isPasteStart()) {
                this.plugin.pastedPrefabPathUUIDMap.put(event.getPrefabId(), new ConcurrentHashMap<UUID, UUID>());
                this.plugin.pastedPrefabPathNameToUUIDMap.put(event.getPrefabId(), new ConcurrentHashMap<String, UUID>());
            }
            else {
                this.plugin.pastedPrefabPathUUIDMap.remove(event.getPrefabId());
                this.plugin.pastedPrefabPathNameToUUIDMap.remove(event.getPrefabId());
            }
        }
    }
    
    public enum Action
    {
        EDIT, 
        EDIT_SELECTION, 
        EDIT_LINE, 
        CUT_COPY, 
        CUT_REMOVE, 
        COPY, 
        PASTE, 
        CLEAR, 
        ROTATE, 
        FLIP, 
        MOVE, 
        STACK, 
        SET, 
        REPLACE, 
        EXTRUDE, 
        UPDATE_SELECTION, 
        WALLS, 
        HOLLOW;
    }
    
    public static class ActionEntry
    {
        private final Action action;
        private final List<SelectionSnapshot<?>> snapshots;
        
        public ActionEntry(final Action action, final SelectionSnapshot<?> snapshots) {
            this(action, Collections.singletonList(snapshots));
        }
        
        public ActionEntry(final Action action, final List<SelectionSnapshot<?>> snapshots) {
            this.action = action;
            this.snapshots = snapshots;
        }
        
        public Action getAction() {
            return this.action;
        }
        
        @Nonnull
        public ActionEntry restore(final Ref<EntityStore> ref, final Player player, final World world, final ComponentAccessor<EntityStore> componentAccessor) {
            List<SelectionSnapshot<?>> collector = Collections.emptyList();
            for (final SelectionSnapshot<?> snapshot : this.snapshots) {
                final SelectionSnapshot<?> nextSnapshot = (SelectionSnapshot<?>)snapshot.restore(ref, player, world, componentAccessor);
                if (nextSnapshot != null) {
                    collector = (collector.isEmpty() ? new ObjectArrayList<SelectionSnapshot<?>>() : collector);
                    collector.add(nextSnapshot);
                }
            }
            return new ActionEntry(this.action, collector);
        }
    }
    
    private static final class QueuedTask
    {
        @Nonnull
        private final ThrowableTriConsumer<Ref<EntityStore>, BuilderState, ComponentAccessor<EntityStore>, ? extends Throwable> task;
        
        private QueuedTask(@Nonnull final ThrowableTriConsumer<Ref<EntityStore>, BuilderState, ComponentAccessor<EntityStore>, ? extends Throwable> biTask) {
            this.task = biTask;
        }
        
        void execute(@Nonnull final Ref<EntityStore> ref, @Nonnull final BuilderState state, @Nonnull final ComponentAccessor<EntityStore> defaultComponentAccessor) throws Throwable {
            this.task.acceptNow(ref, state, defaultComponentAccessor);
        }
    }
    
    public static class BuilderState
    {
        private static final MetricsRegistry<BuilderState> STATE_METRICS_REGISTRY;
        private Player player;
        private PlayerRef playerRef;
        @Nonnull
        private final BuilderToolsUserData userData;
        private final StampedLock undoLock;
        private final ObjectArrayFIFOQueue<ActionEntry> undo;
        private final ObjectArrayFIFOQueue<ActionEntry> redo;
        private final StampedLock taskLock;
        private final ObjectArrayFIFOQueue<QueuedTask> tasks;
        @Nullable
        private volatile CompletableFuture<Void> taskFuture;
        private volatile long timestamp;
        private BlockSelection selection;
        private BlockMask globalMask;
        @Nonnull
        private Random random;
        private UUID activePrefabPath;
        @Nullable
        private Path prefabListRoot;
        @Nullable
        private Path prefabListPath;
        @Nullable
        private String prefabListSearchQuery;
        
        private BuilderState(@Nonnull final Player player, @Nonnull final PlayerRef playerRef) {
            this.undoLock = new StampedLock();
            this.undo = new ObjectArrayFIFOQueue<ActionEntry>();
            this.redo = new ObjectArrayFIFOQueue<ActionEntry>();
            this.taskLock = new StampedLock();
            this.tasks = new ObjectArrayFIFOQueue<QueuedTask>();
            this.timestamp = Long.MAX_VALUE;
            this.random = new Random(26061984L);
            this.player = player;
            this.playerRef = playerRef;
            this.userData = BuilderToolsUserData.get(player);
        }
        
        private void release() {
            this.timestamp = System.nanoTime();
        }
        
        private void retain(@Nonnull final Player player, @Nonnull final PlayerRef playerRef) {
            final long stamp = this.taskLock.writeLock();
            try {
                this.player = player;
                this.playerRef = playerRef;
                this.timestamp = Long.MAX_VALUE;
                if (this.selection != null) {
                    this.sendArea();
                }
            }
            finally {
                this.taskLock.unlockWrite(stamp);
            }
        }
        
        public <T extends Throwable> void addToQueue(@Nonnull final ThrowableTriConsumer<Ref<EntityStore>, BuilderState, ComponentAccessor<EntityStore>, T> task) {
            final long stamp = this.taskLock.writeLock();
            try {
                BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("[%s] Add task with ComponentAccessor to queue %s: %s, %s, %s", this.getDisplayName(), task, this.taskFuture, this.tasks);
                this.tasks.enqueue(new QueuedTask(task));
                if (this.taskFuture == null || this.taskFuture.isDone()) {
                    this.taskFuture = CompletableFutureUtil._catch(CompletableFuture.runAsync(this::runTask, this.player.getWorld()));
                }
            }
            finally {
                this.taskLock.unlockWrite(stamp);
            }
        }
        
        public <T extends Throwable> void computeSelectionCopy(@Nonnull final ThrowableConsumer<BlockSelection, T> task) {
            this.addToQueue((r, b, componentAccessor) -> {
                final long start = System.nanoTime();
                if (this.selection == null) {
                    this.selection = new BlockSelection();
                }
                final BlockSelection oldSelection = this.selection;
                this.pushHistory(Action.COPY, BlockSelectionSnapshot.copyOf(this.selection));
                (this.selection = new BlockSelection()).setPosition(oldSelection.getX(), oldSelection.getY(), oldSelection.getZ());
                this.selection.setSelectionArea(oldSelection.getSelectionMin(), oldSelection.getSelectionMax());
                task.accept(this.selection);
                final long diff = System.nanoTime() - start;
                BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute computeSelectionCopy for %s which copied %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), task, this.selection.getBlockCount());
                this.sendUpdate();
            });
        }
        
        public void runTask() {
            final Ref<EntityStore> ref = this.player.getReference();
            if (ref == null || !ref.isValid()) {
                this.taskFuture = null;
                return;
            }
            final Store<EntityStore> store = ref.getStore();
            while (true) {
                final long stamp = this.taskLock.readLock();
                try {
                    if (this.tasks.isEmpty()) {
                        break;
                    }
                }
                finally {
                    this.taskLock.unlockRead(stamp);
                }
                try {
                    final long stamp2 = this.taskLock.writeLock();
                    QueuedTask task;
                    try {
                        task = this.tasks.dequeue();
                        BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("[%s] Run task from queue: %s, %s, %s", this.getDisplayName(), task, this.taskFuture, this.tasks);
                    }
                    finally {
                        this.taskLock.unlockWrite(stamp2);
                    }
                    task.execute(ref, this, store);
                }
                catch (final Throwable e) {
                    BuilderToolsPlugin.get().getLogger().at(Level.SEVERE).withCause(e).log("Failed to execute builder tools task for: %s", this.getDisplayName());
                }
            }
            this.taskFuture = null;
        }
        
        public int getTaskCount() {
            final long stamp = this.taskLock.readLock();
            try {
                return this.tasks.size();
            }
            finally {
                this.taskLock.unlockRead(stamp);
            }
        }
        
        public int getUndoCount() {
            final long stamp = this.taskLock.readLock();
            try {
                return this.undo.size();
            }
            finally {
                this.taskLock.unlockRead(stamp);
            }
        }
        
        public int getRedoCount() {
            final long stamp = this.taskLock.readLock();
            try {
                return this.redo.size();
            }
            finally {
                this.taskLock.unlockRead(stamp);
            }
        }
        
        public String getDisplayName() {
            return this.playerRef.getUsername();
        }
        
        @Nonnull
        public BuilderToolsUserData getUserData() {
            return this.userData;
        }
        
        @Nullable
        public CompletableFuture<Void> getTaskFuture() {
            return this.taskFuture;
        }
        
        public BlockSelection getSelection() {
            return this.selection;
        }
        
        public BlockMask getGlobalMask() {
            return this.globalMask;
        }
        
        @Nonnull
        public Random getRandom() {
            return this.random;
        }
        
        public void setSelection(@Nonnull final BlockSelection selection) {
            this.selection = selection;
        }
        
        public void sendSelectionToClient() {
            this.sendUpdate();
        }
        
        private void sendErrorFeedback(@Nonnull final Ref<EntityStore> ref, @Nonnull final Message message, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            this.sendFeedback(ref, message, "CREATE_ERROR", NotificationStyle.Warning, componentAccessor);
        }
        
        private void sendFeedback(@Nonnull final Ref<EntityStore> ref, @Nonnull final Message message, @Nullable final String sound, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            this.sendFeedback(message, componentAccessor);
            if (sound != null) {
                SoundUtil.playSoundEvent2d(ref, TempAssetIdUtil.getSoundEventIndex(sound), SoundCategory.UI, componentAccessor);
            }
        }
        
        private void sendFeedback(@Nonnull final Ref<EntityStore> ref, @Nonnull final Message message, @Nullable final String sound, @Nonnull final NotificationStyle notificationStyle, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            this.sendFeedback(message, notificationStyle, componentAccessor);
            if (sound != null) {
                SoundUtil.playSoundEvent2d(ref, TempAssetIdUtil.getSoundEventIndex(sound), SoundCategory.UI, componentAccessor);
            }
        }
        
        private void sendFeedback(@Nonnull final Message message, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            BuilderToolsPlugin.sendFeedback(message, this.player, NotificationStyle.Default, componentAccessor);
        }
        
        private void sendFeedback(@Nonnull final Message message, @Nonnull final NotificationStyle notificationStyle, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            BuilderToolsPlugin.sendFeedback(message, this.player, notificationStyle, componentAccessor);
        }
        
        private void sendFeedback(@Nonnull final String key, final int total, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            BuilderToolsPlugin.sendFeedback(key, total, this.player, componentAccessor);
        }
        
        private void sendFeedback(@Nonnull final String key, final int total, final int num, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            BuilderToolsPlugin.sendFeedback(key, total, num, this.player, componentAccessor);
        }
        
        public void setActivePrefabPath(final UUID path) {
            this.activePrefabPath = path;
        }
        
        public UUID getActivePrefabPath() {
            return this.activePrefabPath;
        }
        
        @Nullable
        public Path getPrefabListRoot() {
            return this.prefabListRoot;
        }
        
        public void setPrefabListRoot(@Nullable final Path prefabListRoot) {
            this.prefabListRoot = prefabListRoot;
        }
        
        @Nullable
        public Path getPrefabListPath() {
            return this.prefabListPath;
        }
        
        public void setPrefabListPath(@Nullable final Path prefabListPath) {
            this.prefabListPath = prefabListPath;
        }
        
        @Nullable
        public String getPrefabListSearchQuery() {
            return this.prefabListSearchQuery;
        }
        
        public void setPrefabListSearchQuery(@Nullable final String prefabListSearchQuery) {
            this.prefabListSearchQuery = prefabListSearchQuery;
        }
        
        public int edit(@Nonnull final Ref<EntityStore> ref, @Nonnull final BuilderToolOnUseInteraction packet, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            final World world = componentAccessor.getExternalData().getWorld();
            final UUIDComponent uuidComponent = componentAccessor.getComponent(ref, UUIDComponent.getComponentType());
            assert uuidComponent != null;
            final long start = System.nanoTime();
            ToolOperation toolOperation;
            try {
                toolOperation = ToolOperation.fromPacket(ref, this.player, packet, componentAccessor);
            }
            catch (final Exception e) {
                this.player.sendMessage(Message.translation("server.builderTools.interaction.toolParseError").param("error", e.getMessage()));
                return 0;
            }
            final PrototypePlayerBuilderToolSettings protoSettings = ToolOperation.PROTOTYPE_TOOL_SETTINGS.get(uuidComponent.getUuid());
            if (protoSettings != null && toolOperation instanceof PaintOperation) {
                final BuilderTool builderTool = BuilderTool.getActiveBuilderTool(this.player);
                if (protoSettings.isLoadingBrush()) {
                    return 0;
                }
                if (builderTool != null && builderTool.getBrushConfigurationCommand() != null && !builderTool.getBrushConfigurationCommand().isEmpty()) {
                    final String brushConfigId = builderTool.getBrushConfigurationCommand();
                    final String loadedBrushConfig = protoSettings.getCurrentlyLoadedBrushConfigName();
                    if (loadedBrushConfig.equalsIgnoreCase(brushConfigId)) {
                        toolOperation.executeAsBrushConfig(protoSettings, packet, componentAccessor);
                    }
                    else {
                        final ScriptedBrushAsset scriptedBrush = ScriptedBrushAsset.get(brushConfigId);
                        if (scriptedBrush != null) {
                            protoSettings.setCurrentlyLoadedBrushConfigName(brushConfigId);
                            final BrushConfigCommandExecutor brushConfigCommandExecutor = protoSettings.getBrushConfigCommandExecutor();
                            scriptedBrush.loadIntoExecutor(brushConfigCommandExecutor);
                            protoSettings.setUsePrototypeBrushConfigurations(false);
                            toolOperation.executeAsBrushConfig(protoSettings, packet, componentAccessor);
                        }
                        else {
                            protoSettings.setCurrentlyLoadedBrushConfigName(brushConfigId);
                            final BrushConfigCommandExecutor brushConfigCommandExecutor = protoSettings.getBrushConfigCommandExecutor();
                            brushConfigCommandExecutor.getSequentialOperations().clear();
                            brushConfigCommandExecutor.getGlobalOperations().clear();
                            protoSettings.setLoadingBrush(true);
                            CommandManager.get().handleCommand(this.player, brushConfigId).thenAccept(unused -> {
                                final PrototypePlayerBuilderToolSettings protoSettingsIntl = ToolOperation.PROTOTYPE_TOOL_SETTINGS.get(uuidComponent.getUuid());
                                protoSettingsIntl.setLoadingBrush(false);
                                protoSettingsIntl.setUsePrototypeBrushConfigurations(false);
                                toolOperation.executeAsBrushConfig(protoSettingsIntl, packet, componentAccessor);
                                return;
                            });
                        }
                    }
                    return 0;
                }
                if (protoSettings.usePrototypeBrushConfigurations()) {
                    toolOperation.executeAsBrushConfig(protoSettings, packet, componentAccessor);
                    return 0;
                }
            }
            final Vector3i currentPosition = toolOperation.getPosition();
            final Vector3i lastPosition = (protoSettings != null && packet.isHoldDownInteraction) ? protoSettings.getLastBrushPosition() : null;
            final List<Vector3i> positionsToExecute = ToolOperation.calculateInterpolatedPositions(lastPosition, currentPosition, toolOperation.getBrushWidth(), toolOperation.getBrushHeight(), toolOperation.getBrushSpacing());
            if (positionsToExecute.isEmpty()) {
                return 0;
            }
            for (final Vector3i position : positionsToExecute) {
                toolOperation.executeAt(position.getX(), position.getY(), position.getZ(), componentAccessor);
            }
            if (protoSettings != null) {
                if (packet.isHoldDownInteraction) {
                    protoSettings.setLastBrushPosition(positionsToExecute.get(positionsToExecute.size() - 1));
                }
                else {
                    protoSettings.clearLastBrushPosition();
                }
            }
            final EditOperation edit = toolOperation.getEditOperation();
            final BlockSelection before = edit.getBefore();
            final BlockSelection after = edit.getAfter();
            this.pushHistory(Action.EDIT, new BlockSelectionSnapshot(before));
            after.placeNoReturn("Use Builder Tool ?/?", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            final int size = after.getBlockCount();
            final int interpolatedCount = positionsToExecute.size();
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute edit of %d blocks (%d positions)", diff, TimeUnit.NANOSECONDS.toMillis(diff), size, interpolatedCount);
            if (protoSettings != null && protoSettings.isShouldShowEditorSettings()) {
                this.sendFeedback("Edit", size, componentAccessor);
            }
            return size;
        }
        
        public void placeBrushConfig(@Nonnull final Ref<EntityStore> ref, final long startTime, @Nonnull final BrushConfigEditStore brushConfigEditStore, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
            assert playerRefComponent != null;
            final World world = componentAccessor.getExternalData().getWorld();
            final BlockSelection after = brushConfigEditStore.getAfter();
            final BlockSelection before = brushConfigEditStore.getBefore();
            this.pushHistory(Action.EDIT, new BlockSelectionSnapshot(before));
            after.placeNoReturn("Use Builder Tool ?/?", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - startTime;
            final int size = after.getBlockCount();
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute edit of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), size);
            final PrototypePlayerBuilderToolSettings prototypePlayerBuilderToolSettings = ToolOperation.PROTOTYPE_TOOL_SETTINGS.get(playerRefComponent.getUuid());
            if (prototypePlayerBuilderToolSettings != null && prototypePlayerBuilderToolSettings.isShouldShowEditorSettings()) {
                this.sendFeedback("Edit", size, componentAccessor);
            }
        }
        
        public void flood(@Nonnull final EditOperation editOperation, final int x, final int y, final int z, final int shapeWidth, final int shapeHeight, @Nonnull final BlockPattern pattern, final int targetBlockId) {
            final int halfWidth = shapeWidth / 2;
            final int halfHeight = shapeHeight / 2;
            final Vector3i min = new Vector3i(x - halfWidth, y - halfHeight, z - halfWidth);
            final Vector3i max = new Vector3i(x + halfWidth, y + halfHeight, z + halfWidth);
            final OverridableChunkAccessor accessor = editOperation.getAccessor();
            final LongOpenHashSet checkedPositions = new LongOpenHashSet();
            final LongArrayList floodPositions = new LongArrayList();
            floodPositions.push(BlockUtil.pack(x, y, z));
            do {
                final long packedPosition = floodPositions.popLong();
                checkedPositions.add(packedPosition);
                final int px = BlockUtil.unpackX(packedPosition);
                final int py = BlockUtil.unpackY(packedPosition);
                final int pz = BlockUtil.unpackZ(packedPosition);
                final int blockId = pattern.nextBlock(this.random);
                final long east = BlockUtil.pack(px + 1, py, pz);
                if (this.isFloodPossible(accessor, east, min, max, blockId, targetBlockId) && !checkedPositions.contains(east)) {
                    floodPositions.push(east);
                }
                final long west = BlockUtil.pack(px - 1, py, pz);
                if (this.isFloodPossible(accessor, west, min, max, blockId, targetBlockId) && !checkedPositions.contains(west)) {
                    floodPositions.push(west);
                }
                final long top = BlockUtil.pack(px, py + 1, pz);
                if (this.isFloodPossible(accessor, top, min, max, blockId, targetBlockId) && !checkedPositions.contains(top)) {
                    floodPositions.push(top);
                }
                final long bottom = BlockUtil.pack(px, py - 1, pz);
                if (this.isFloodPossible(accessor, bottom, min, max, blockId, targetBlockId) && !checkedPositions.contains(bottom)) {
                    floodPositions.push(bottom);
                }
                final long north = BlockUtil.pack(px, py, pz + 1);
                if (this.isFloodPossible(accessor, north, min, max, blockId, targetBlockId) && !checkedPositions.contains(north)) {
                    floodPositions.push(north);
                }
                final long south = BlockUtil.pack(px, py, pz - 1);
                if (this.isFloodPossible(accessor, south, min, max, blockId, targetBlockId) && !checkedPositions.contains(south)) {
                    floodPositions.push(south);
                }
                if (this.isFloodPossible(accessor, packedPosition, min, max, blockId, targetBlockId)) {
                    editOperation.setBlock(px, py, pz, blockId);
                }
            } while (!floodPositions.isEmpty());
        }
        
        private boolean isFloodPossible(@Nonnull final ChunkAccessor accessor, final long blockPosition, @Nonnull final Vector3i min, @Nonnull final Vector3i max, final int blockId, final int targetBlockId) {
            final int x = BlockUtil.unpackX(blockPosition);
            final int y = BlockUtil.unpackY(blockPosition);
            final int z = BlockUtil.unpackZ(blockPosition);
            if (x < min.getX() || y < min.getY() || z < min.getZ() || x > max.getX() || y > max.getY() || z > max.getZ()) {
                return false;
            }
            final BlockTypeAssetMap<String, BlockType> assetMap = BlockType.getAssetMap();
            final BlockType blockType = assetMap.getAsset(accessor.getBlock(x, y, z));
            return accessor.getBlock(x, y, z) == targetBlockId || (blockType.getDrawType() != DrawType.Cube && blockType.getDrawType() != DrawType.CubeWithModel);
        }
        
        public boolean isAsideAir(@Nonnull final ChunkAccessor accessor, final int x, final int y, final int z) {
            return accessor.getBlock(x + 1, y, z) <= 0 || accessor.getBlock(x - 1, y, z) <= 0 || accessor.getBlock(x, y + 1, z) <= 0 || accessor.getBlock(x, y - 1, z) <= 0 || accessor.getBlock(x, y, z + 1) <= 0 || accessor.getBlock(x, y, z - 1) <= 0;
        }
        
        public boolean isAsideBlock(@Nonnull final ChunkAccessor accessor, final int x, final int y, final int z) {
            return accessor.getBlock(x, y, z) <= 0 && (accessor.getBlock(x + 1, y, z) > 0 || accessor.getBlock(x - 1, y, z) > 0 || accessor.getBlock(x, y + 1, z) > 0 || accessor.getBlock(x, y - 1, z) > 0 || accessor.getBlock(x, y, z + 1) > 0 || accessor.getBlock(x, y, z - 1) > 0);
        }
        
        @Nonnull
        public BlocksSampleData getBlocksSampleData(@Nonnull final ChunkAccessor accessor, final int x, final int y, final int z, final int radius) {
            final BlocksSampleData data = new BlocksSampleData();
            final Int2IntMap blockCounts = new Int2IntOpenHashMap();
            for (int ix = x - radius; ix <= x + radius; ++ix) {
                for (int iz = z - radius; iz <= z + radius; ++iz) {
                    for (int iy = y - radius; iy <= y + radius; ++iy) {
                        final int currentBlock = accessor.getBlock(ix, iy, iz);
                        blockCounts.put(currentBlock, blockCounts.getOrDefault(currentBlock, 0) + 1);
                    }
                }
            }
            final BlockTypeAssetMap<String, BlockType> assetMap = BlockType.getAssetMap();
            for (final Int2IntMap.Entry pair : Int2IntMaps.fastIterable(blockCounts)) {
                final int block = pair.getIntKey();
                final int count = pair.getIntValue();
                if (count > data.mainBlockCount) {
                    data.mainBlock = block;
                    data.mainBlockCount = count;
                }
                final BlockType blockType = assetMap.getAsset(block);
                if (count > data.mainBlockNotAirCount && block != 0) {
                    data.mainBlockNotAir = block;
                    data.mainBlockNotAirCount = count;
                }
            }
            return data;
        }
        
        @Nonnull
        public SmoothSampleData getBlocksSmoothData(@Nonnull final ChunkAccessor accessor, final int x, final int y, final int z) {
            final SmoothSampleData data = new SmoothSampleData();
            final Int2IntMap blockCounts = new Int2IntOpenHashMap();
            int kernelIndex = 0;
            for (int ix = x - 1; ix <= x + 1; ++ix) {
                for (int iy = y - 1; iy <= y + 1; ++iy) {
                    for (int iz = z - 1; iz <= z + 1; ++iz) {
                        final int currentBlock = accessor.getBlock(ix, iy, iz);
                        blockCounts.put(currentBlock, blockCounts.getOrDefault(currentBlock, 0) + BuilderToolsPlugin.SMOOTHING_KERNEL[kernelIndex++]);
                    }
                }
            }
            float solidCount = 0.0f;
            final BlockTypeAssetMap<String, BlockType> assetMap = BlockType.getAssetMap();
            for (final Int2IntMap.Entry pair : Int2IntMaps.fastIterable(blockCounts)) {
                final int block = pair.getIntKey();
                final int count = pair.getIntValue();
                final BlockType blockType = assetMap.getAsset(block);
                if (blockType.getMaterial() == BlockMaterial.Solid) {
                    solidCount += count;
                    if (count <= data.solidBlockCount) {
                        continue;
                    }
                    data.solidBlock = block;
                    data.solidBlockCount = count;
                }
                else {
                    if (count <= data.fillerBlockCount) {
                        continue;
                    }
                    data.fillerBlock = block;
                    data.fillerBlockCount = count;
                }
            }
            data.solidStrength = solidCount / 27.0f;
            return data;
        }
        
        public void editLine(final int x1, final int y1, final int z1, final int x2, final int y2, final int z2, final BlockPattern material, final int lineWidth, final int lineHeight, final int wallThickness, final BrushShape shape, final BrushOrigin origin, final int spacing, final int density, final ComponentAccessor<EntityStore> componentAccessor) {
            this.editLine(x1, y1, z1, x2, y2, z2, material, lineWidth, lineHeight, wallThickness, shape, origin, spacing, density, this.getGlobalMask(), componentAccessor);
        }
        
        public void editLine(final int x1, final int y1, final int z1, final int x2, final int y2, final int z2, final BlockPattern material, final int lineWidth, final int lineHeight, final int wallThickness, final BrushShape shape, final BrushOrigin origin, final int spacing, final int density, @Nullable final BlockMask mask, final ComponentAccessor<EntityStore> componentAccessor) {
            final World world = componentAccessor.getExternalData().getWorld();
            final long start = System.nanoTime();
            final float halfWidth = lineWidth / 2.0f;
            final float halfHeight = lineHeight / 2.0f;
            final int iHalfWidth = MathUtil.fastCeil(halfWidth);
            final int iHalfHeight = MathUtil.fastCeil(halfHeight);
            final int maxRadius = Math.max(iHalfWidth, iHalfHeight);
            final Vector3i min = new Vector3i(Math.min(x1, x2) - maxRadius, Math.min(y1, y2) - maxRadius, Math.min(z1, z2) - maxRadius);
            final Vector3i max = new Vector3i(Math.max(x1, x2) + maxRadius, Math.max(y1, y2) + maxRadius, Math.max(z1, z2) + maxRadius);
            final BlockSelection before = new BlockSelection();
            before.setPosition(x1, y1, z1);
            before.setSelectionArea(min, max);
            this.pushHistory(Action.EDIT_LINE, new BlockSelectionSnapshot(before));
            final BlockSelection after = new BlockSelection(before);
            int originOffset = 0;
            if (origin == BrushOrigin.Bottom) {
                originOffset = iHalfHeight + 1;
            }
            else if (origin == BrushOrigin.Top) {
                originOffset = -iHalfHeight;
            }
            final float innerHalfWidth = Math.max(0.0f, halfWidth - wallThickness);
            final float innerHalfHeight = Math.max(0.0f, halfHeight - wallThickness);
            final Predicate<Vector3i> isInShape = this.createShapePredicate(shape, halfWidth, halfHeight, innerHalfWidth, innerHalfHeight, wallThickness > 0);
            final int lineDistX = x2 - x1;
            final int lineDistZ = z2 - z1;
            final int halfLineDistX = lineDistX / 2;
            final int halfLineDistZ = lineDistZ / 2;
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, x1 + halfLineDistX, z1 + halfLineDistZ, Math.max(Math.abs(lineDistX), Math.abs(lineDistZ)) + maxRadius + 1);
            final Vector3i rel = new Vector3i();
            final LineIterator line = new LineIterator(x1, y1, z1, x2, y2, z2);
            int stepCount = 0;
            while (line.hasNext()) {
                final Vector3i coord = line.next();
                if (stepCount % spacing != 0) {
                    ++stepCount;
                }
                else {
                    ++stepCount;
                    for (int sx = -iHalfWidth; sx <= iHalfWidth; ++sx) {
                        for (int sz = iHalfWidth; sz >= -iHalfWidth; --sz) {
                            final int blockX = coord.getX() + sx;
                            final int blockZ = coord.getZ() + sz;
                            final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(blockX, blockZ));
                            for (int sy = -iHalfHeight; sy <= iHalfHeight; ++sy) {
                                rel.assign(sx, sy, sz);
                                if (isInShape.test(rel)) {
                                    final int blockY = coord.getY() + sy + originOffset;
                                    final int currentBlockId = chunk.getBlock(blockX, blockY, blockZ);
                                    final int currentFluidId = chunk.getFluidId(blockX, blockY, blockZ);
                                    if (mask == null || !mask.isExcluded(accessor, blockX, blockY, blockZ, min, max, currentBlockId, currentFluidId)) {
                                        if (this.random.nextInt(100) < density) {
                                            final int blockId = material.nextBlock(this.random);
                                            before.addBlockAtWorldPos(blockX, blockY, blockZ, currentBlockId, chunk.getRotationIndex(blockX, blockY, blockZ), chunk.getFiller(blockX, blockY, blockZ), chunk.getSupportValue(blockX, blockY, blockZ), chunk.getBlockComponentHolder(blockX, blockY, blockZ));
                                            after.addBlockAtWorldPos(blockX, blockY, blockZ, blockId, 0, 0, 0);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            after.placeNoReturn("Edit 1/1", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            final int size = after.getBlockCount();
            final double length = new Vector3i(x1, y1, z1).distanceTo(x2, y2, z2);
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute editLine of %d blocks with length %s", diff, TimeUnit.NANOSECONDS.toMillis(diff), size, length);
            this.sendFeedback(Message.translation("server.builderTools.drawLineOf").param("count", length), componentAccessor);
        }
        
        private Predicate<Vector3i> createShapePredicate(final BrushShape shape, final float halfWidth, final float halfHeight, final float innerHalfWidth, final float innerHalfHeight, final boolean hollow) {
            final float hw = halfWidth + 0.41f;
            final float hh = halfHeight + 0.41f;
            final float ihw = innerHalfWidth + 0.41f;
            final float ihh = innerHalfHeight + 0.41f;
            return switch (shape) {
                default -> throw new MatchException(null, null);
                case Cube -> coord -> {
                    final double ax = Math.abs(coord.getX());
                    final double ay = Math.abs(coord.getY());
                    final double az = Math.abs(coord.getZ());
                    final boolean inOuter = ax <= hw && ay <= hh && az <= hw;
                    if (!hollow) {
                        return inOuter;
                    }
                    else {
                        final boolean inInner = ax < ihw && ay < ihh && az < ihw;
                        return inOuter && !inInner;
                    }
                };
                case Sphere -> coord -> {
                    final double sx = coord.getX();
                    final double sy = coord.getY();
                    final double sz = coord.getZ();
                    final double outerDist = sx * sx / (hw * hw) + sy * sy / (hh * hh) + sz * sz / (hw * hw);
                    final boolean inOuter2 = outerDist <= 1.0;
                    if (!hollow) {
                        return inOuter2;
                    }
                    else {
                        final double innerDist = sx * sx / (ihw * ihw) + sy * sy / (ihh * ihh) + sz * sz / (ihw * ihw);
                        final boolean inInner2 = ihw > 0.0f && ihh > 0.0f && innerDist <= 1.0;
                        return inOuter2 && !inInner2;
                    }
                };
                case Cylinder -> coord -> {
                    final double sx2 = coord.getX();
                    final double sy2 = coord.getY();
                    final double sz2 = coord.getZ();
                    final double outerRadialDist = (sx2 * sx2 + sz2 * sz2) / (hw * hw);
                    final boolean inOuterRadius = outerRadialDist <= 1.0 && Math.abs(sy2) <= hh;
                    if (!hollow) {
                        return inOuterRadius;
                    }
                    else {
                        final double innerRadialDist = (sx2 * sx2 + sz2 * sz2) / (ihw * ihw);
                        final boolean inInnerRadius = ihw > 0.0f && innerRadialDist <= 1.0 && Math.abs(sy2) < ihh;
                        return inOuterRadius && !inInnerRadius;
                    }
                };
                case Cone -> coord -> {
                    final double sx3 = coord.getX();
                    final double sy3 = coord.getY();
                    final double sz3 = coord.getZ();
                    final double normalizedY = (sy3 + hh) / (2.0f * hh);
                    if (normalizedY < 0.0 || normalizedY > 1.0) {
                        return false;
                    }
                    else {
                        final double currentRadius = hw * (1.0 - normalizedY);
                        final double radialDist = Math.sqrt(sx3 * sx3 + sz3 * sz3);
                        final boolean inOuter3 = radialDist <= currentRadius;
                        if (!hollow) {
                            return inOuter3;
                        }
                        else {
                            final double innerRadius = Math.max(0.0, currentRadius - (hw - ihw));
                            final boolean inInner3 = radialDist < innerRadius;
                            return inOuter3 && !inInner3;
                        }
                    }
                };
                case InvertedCone -> coord -> {
                    final double sx4 = coord.getX();
                    final double sy4 = coord.getY();
                    final double sz4 = coord.getZ();
                    final double normalizedY2 = (sy4 + hh) / (2.0f * hh);
                    if (normalizedY2 < 0.0 || normalizedY2 > 1.0) {
                        return false;
                    }
                    else {
                        final double currentRadius2 = hw * normalizedY2;
                        final double radialDist2 = Math.sqrt(sx4 * sx4 + sz4 * sz4);
                        final boolean inOuter4 = radialDist2 <= currentRadius2;
                        if (!hollow) {
                            return inOuter4;
                        }
                        else {
                            final double innerRadius2 = Math.max(0.0, currentRadius2 - (hw - ihw));
                            final boolean inInner4 = radialDist2 < innerRadius2;
                            return inOuter4 && !inInner4;
                        }
                    }
                };
                case Pyramid -> coord -> {
                    final double sx5 = coord.getX();
                    final double sy5 = coord.getY();
                    final double sz5 = coord.getZ();
                    final double normalizedY3 = (sy5 + hh) / (2.0f * hh);
                    if (normalizedY3 < 0.0 || normalizedY3 > 1.0) {
                        return false;
                    }
                    else {
                        final double currentHalfSize = hw * (1.0 - normalizedY3);
                        final boolean inOuter5 = Math.abs(sx5) <= currentHalfSize && Math.abs(sz5) <= currentHalfSize;
                        if (!hollow) {
                            return inOuter5;
                        }
                        else {
                            final double innerHalfSize = Math.max(0.0, currentHalfSize - (hw - ihw));
                            final boolean inInner5 = Math.abs(sx5) < innerHalfSize && Math.abs(sz5) < innerHalfSize;
                            return inOuter5 && !inInner5;
                        }
                    }
                };
                case InvertedPyramid -> coord -> {
                    final double sx6 = coord.getX();
                    final double sy6 = coord.getY();
                    final double sz6 = coord.getZ();
                    final double normalizedY4 = (sy6 + hh) / (2.0f * hh);
                    if (normalizedY4 < 0.0 || normalizedY4 > 1.0) {
                        return false;
                    }
                    else {
                        final double currentHalfSize2 = hw * normalizedY4;
                        final boolean inOuter6 = Math.abs(sx6) <= currentHalfSize2 && Math.abs(sz6) <= currentHalfSize2;
                        if (!hollow) {
                            return inOuter6;
                        }
                        else {
                            final double innerHalfSize2 = Math.max(0.0, currentHalfSize2 - (hw - ihw));
                            final boolean inInner6 = Math.abs(sx6) < innerHalfSize2 && Math.abs(sz6) < innerHalfSize2;
                            return inOuter6 && !inInner6;
                        }
                    }
                };
                case Dome -> coord -> {
                    final double sx7 = coord.getX();
                    final double sy7 = coord.getY();
                    final double sz7 = coord.getZ();
                    if (sy7 < 0.0) {
                        return false;
                    }
                    else {
                        final double outerDist2 = sx7 * sx7 / (hw * hw) + sy7 * sy7 / (hh * hh) + sz7 * sz7 / (hw * hw);
                        final boolean inOuter7 = outerDist2 <= 1.0;
                        if (!hollow) {
                            return inOuter7;
                        }
                        else {
                            final double innerDist2 = sx7 * sx7 / (ihw * ihw) + sy7 * sy7 / (ihh * ihh) + sz7 * sz7 / (ihw * ihw);
                            final boolean inInner7 = ihw > 0.0f && ihh > 0.0f && innerDist2 <= 1.0;
                            return inOuter7 && !inInner7;
                        }
                    }
                };
                case InvertedDome -> coord -> {
                    final double sx8 = coord.getX();
                    final double sy8 = coord.getY();
                    final double sz8 = coord.getZ();
                    if (sy8 > 0.0) {
                        return false;
                    }
                    else {
                        final double outerDist3 = sx8 * sx8 / (hw * hw) + sy8 * sy8 / (hh * hh) + sz8 * sz8 / (hw * hw);
                        final boolean inOuter8 = outerDist3 <= 1.0;
                        if (!hollow) {
                            return inOuter8;
                        }
                        else {
                            final double innerDist3 = sx8 * sx8 / (ihw * ihw) + sy8 * sy8 / (ihh * ihh) + sz8 * sz8 / (ihw * ihw);
                            final boolean inInner8 = ihw > 0.0f && ihh > 0.0f && innerDist3 <= 1.0;
                            return inOuter8 && !inInner8;
                        }
                    }
                };
                case Diamond -> coord -> {
                    final double sx9 = coord.getX();
                    final double sy9 = coord.getY();
                    final double sz9 = coord.getZ();
                    final double normalizedY5 = Math.abs(sy9) / hh;
                    if (normalizedY5 > 1.0) {
                        return false;
                    }
                    else {
                        final double currentHalfSize3 = hw * (1.0 - normalizedY5);
                        final boolean inOuter9 = Math.abs(sx9) <= currentHalfSize3 && Math.abs(sz9) <= currentHalfSize3;
                        if (!hollow) {
                            return inOuter9;
                        }
                        else {
                            final double innerHalfSize3 = Math.max(0.0, currentHalfSize3 - (hw - ihw));
                            final boolean inInner9 = Math.abs(sx9) < innerHalfSize3 && Math.abs(sz9) < innerHalfSize3;
                            return inOuter9 && !inInner9;
                        }
                    }
                };
                case Torus -> coord -> {
                    final double sx10 = coord.getX();
                    final double sy10 = coord.getY();
                    final double sz10 = coord.getZ();
                    final double minorRadius = Math.max(1.0f, hh / 2.0f);
                    final double majorRadius = Math.max(1.0, hw - minorRadius);
                    final double minorRadiusAdjusted = minorRadius + 0.4099999964237213;
                    final double distFromCenter = Math.sqrt(sx10 * sx10 + sz10 * sz10);
                    final double distFromRing = distFromCenter - majorRadius;
                    final double distFromTube = Math.sqrt(distFromRing * distFromRing + sy10 * sy10);
                    final boolean inOuter10 = distFromTube <= minorRadiusAdjusted;
                    if (!hollow) {
                        return inOuter10;
                    }
                    else {
                        final double innerMinorRadius = Math.max(0.0, minorRadiusAdjusted - ((ihw > 0.0f) ? (hw - ihw) : 0.0f));
                        final boolean inInner10 = innerMinorRadius > 0.0 && distFromTube < innerMinorRadius;
                        return inOuter10 && !inInner10;
                    }
                };
            };
        }
        
        public void extendFace(final int x, final int y, final int z, final int normalX, final int normalY, final int normalZ, final int extrudeDepth, final int radiusAllowed, final int blockId, @Nullable Vector3i min, @Nullable Vector3i max, final ComponentAccessor<EntityStore> componentAccessor) {
            final World world = componentAccessor.getExternalData().getWorld();
            final long start = System.nanoTime();
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, x, z, radiusAllowed);
            if (min == null) {
                min = new Vector3i(x - radiusAllowed, y - radiusAllowed, z - radiusAllowed);
            }
            else {
                int minX = min.getX();
                if (x - radiusAllowed > minX) {
                    minX = x - radiusAllowed;
                }
                int minY = min.getY();
                if (y - radiusAllowed > minY) {
                    minY = y - radiusAllowed;
                }
                int minZ = min.getZ();
                if (z - radiusAllowed > minZ) {
                    minZ = z - radiusAllowed;
                }
                min = new Vector3i(minX, minY, minZ);
            }
            if (max == null) {
                max = new Vector3i(x + radiusAllowed, y + radiusAllowed, z + radiusAllowed);
            }
            else {
                int maxX = max.getX();
                if (x + radiusAllowed < maxX) {
                    maxX = x + radiusAllowed;
                }
                int maxY = max.getY();
                if (y + radiusAllowed < maxY) {
                    maxY = y + radiusAllowed;
                }
                int maxZ = max.getZ();
                if (z + radiusAllowed < maxZ) {
                    maxZ = z + radiusAllowed;
                }
                max = new Vector3i(maxX, maxY, maxZ);
            }
            final int totalBlocks = (max.getX() - min.getX() + 1) * (max.getZ() - min.getZ() + 1) * (max.getY() - min.getY() + 1);
            final BlockSelection before = new BlockSelection(totalBlocks, 0);
            before.setPosition(x + normalX, y + normalY, z + normalZ);
            before.setSelectionArea(min, max);
            this.pushHistory(Action.EXTRUDE, new BlockSelectionSnapshot(before));
            final BlockSelection after = new BlockSelection(totalBlocks, 0);
            after.copyPropertiesFrom(before);
            this.extendFaceFindBlocks(accessor, BlockType.getAssetMap(), before, after, x + normalX, y + normalY, z + normalZ, normalX, normalY, normalZ, extrudeDepth, blockId, min, max);
            final Vector3i offset = new Vector3i(0, 0, 0);
            for (int i = 0; i < extrudeDepth; ++i) {
                offset.x = normalX * i;
                offset.y = normalY * i;
                offset.z = normalZ * i;
                after.placeNoReturn("Set", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, offset, null, componentAccessor);
            }
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute set of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), after.getBlockCount());
            this.sendUpdate();
            this.sendArea();
        }
        
        private void extendFaceFindBlocks(@Nonnull final ChunkAccessor accessor, @Nonnull final BlockTypeAssetMap<String, BlockType> assetMap, @Nonnull final BlockSelection before, @Nonnull final BlockSelection after, final int x, final int y, final int z, final int normalX, final int normalY, final int normalZ, final int extrudeDepth, final int blockId, @Nonnull final Vector3i min, @Nonnull final Vector3i max) {
            if (x < min.getX() || x > max.getX()) {
                return;
            }
            if (y < min.getY() || y > max.getY()) {
                return;
            }
            if (z < min.getZ() || z > max.getZ()) {
                return;
            }
            final int block = accessor.getBlock(x, y, z);
            final int testBlock = accessor.getBlock(x - normalX, y - normalY, z - normalZ);
            final BlockType testBlockType = assetMap.getAsset(testBlock);
            if (testBlockType == null || (testBlockType.getDrawType() != DrawType.Cube && testBlockType.getDrawType() != DrawType.CubeWithModel)) {
                return;
            }
            if (before.hasBlockAtWorldPos(x, y, z)) {
                return;
            }
            final BlockAccessor blocks = accessor.getChunkIfInMemory(ChunkUtil.indexChunkFromBlock(x, z));
            if (blocks == null) {
                return;
            }
            before.addBlockAtWorldPos(x, y, z, block, blocks.getRotationIndex(x, y, z), blocks.getFiller(x, y, z), blocks.getSupportValue(x, y, z), blocks.getBlockComponentHolder(x, y, z));
            after.addBlockAtWorldPos(x, y, z, blockId, 0, 0, 0);
            for (final Vector3i side : Vector3i.BLOCK_SIDES) {
                if ((normalX == 0 || side.getX() == 0) && (normalY == 0 || side.getY() == 0)) {
                    if (normalZ == 0 || side.getZ() == 0) {
                        this.extendFaceFindBlocks(accessor, assetMap, before, after, x + side.getX(), y + side.getY(), z + side.getZ(), normalX, normalY, normalZ, extrudeDepth, blockId, min, max);
                    }
                }
            }
        }
        
        public void update(final int xMin, final int yMin, final int zMin, final int xMax, final int yMax, final int zMax) {
            if (this.selection == null) {
                this.selection = new BlockSelection();
            }
            this.pushHistory(Action.UPDATE_SELECTION, new ClipboardBoundsSnapshot(this.selection));
            this.selection.setSelectionArea(new Vector3i(xMin, yMin, zMin), new Vector3i(xMax, yMax, zMax));
        }
        
        public void tint(@Nonnull final Ref<EntityStore> ref, final int color, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final World world = componentAccessor.getExternalData().getWorld();
            int count = 0;
            final int minX = this.selection.getSelectionMin().getX();
            final int minZ = this.selection.getSelectionMin().getZ();
            final int maxX = this.selection.getSelectionMax().getX();
            final int maxZ = this.selection.getSelectionMax().getZ();
            for (int cx = ChunkUtil.chunkCoordinate(minX); cx <= ChunkUtil.chunkCoordinate(maxX); ++cx) {
                for (int cz = ChunkUtil.chunkCoordinate(minZ); cz <= ChunkUtil.chunkCoordinate(maxZ); ++cz) {
                    final int startX = Math.max(0, minX - ChunkUtil.minBlock(cx));
                    final int startZ = Math.max(0, minZ - ChunkUtil.minBlock(cz));
                    final int endX = Math.min(32, maxX - ChunkUtil.minBlock(cx));
                    final int endZ = Math.min(32, maxZ - ChunkUtil.minBlock(cz));
                    final WorldChunk chunk = world.getNonTickingChunk(ChunkUtil.indexChunk(cx, cz));
                    for (int z = startZ; z < endZ; ++z) {
                        for (int x = startX; x < endX; ++x) {
                            chunk.getBlockChunk().setTint(x, z, color);
                            ++count;
                        }
                    }
                    world.getNotificationHandler().updateChunk(chunk.getIndex());
                }
            }
            this.sendFeedback(Message.translation("server.builderTools.setColumnsTint").param("count", count), componentAccessor);
        }
        
        public void tint(final int x, final int y, final int z, final int color, @Nonnull final BrushShape shape, final int shapeRange, final int shapeHeight, final ComponentAccessor<EntityStore> componentAccessor) {
            if (y < 0 || y >= 320) {
                return;
            }
            final World world = componentAccessor.getExternalData().getWorld();
            final LongSet dirtyChunks = new LongOpenHashSet();
            final AtomicInteger count = new AtomicInteger(0);
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, x, z, shapeRange + 1);
            int px = 0;
            int pz = 0;
            final TriIntObjPredicate<Void> tintBlock = (px, py, pz, aVoid) -> {
                final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(px, pz));
                chunk.getBlockChunk().setTint(px, pz, color);
                dirtyChunks.add(chunk.getIndex());
                count.getAndIncrement();
                return true;
            };
            Label_0396: {
                if (shapeRange <= 1) {
                    tintBlock.test(x, y, z, null);
                }
                else {
                    final int radiusXZ = shapeRange / 2;
                    switch (shape) {
                        case Cube:
                        case Pyramid:
                        case InvertedPyramid:
                        case Diamond: {
                            for (px = -radiusXZ; px <= radiusXZ; ++px) {
                                for (pz = -radiusXZ; pz <= radiusXZ; ++pz) {
                                    if (!tintBlock.test(x + px, y, z + pz, null)) {
                                        break Label_0396;
                                    }
                                }
                            }
                            break;
                        }
                        case Sphere:
                        case Cylinder:
                        case Cone:
                        case InvertedCone:
                        case Dome:
                        case InvertedDome: {
                            BlockSphereUtil.forEachBlock(x, y, z, radiusXZ, 1, radiusXZ, null, tintBlock);
                            break;
                        }
                        case Torus: {
                            final int minorRadius = Math.max(1, shapeHeight / 4);
                            final int outerRadius = radiusXZ;
                            final int majorRadius = Math.max(1, outerRadius - minorRadius);
                            final int sizeXZ = majorRadius + minorRadius;
                            final float minorRadiusAdjusted = minorRadius + 0.5f;
                            for (int px2 = -sizeXZ; px2 <= sizeXZ; ++px2) {
                                for (int pz2 = -sizeXZ; pz2 <= sizeXZ; ++pz2) {
                                    final double distFromCenter = Math.sqrt(px2 * px2 + pz2 * pz2);
                                    final double distFromRing = Math.abs(distFromCenter - majorRadius);
                                    if (distFromRing <= minorRadiusAdjusted) {
                                        tintBlock.test(x + px2, y, z + pz2, null);
                                    }
                                }
                            }
                            break;
                        }
                        default: {
                            this.sendFeedback(Message.translation("server.builderTools.errorWithUsedShape"), componentAccessor);
                            return;
                        }
                    }
                }
            }
            dirtyChunks.forEach(value -> world.getNotificationHandler().updateChunk(value));
            this.sendFeedback(Message.translation("server.builderTools.setColumnsTint").param("count", count.intValue()), componentAccessor);
        }
        
        public void environment(@Nonnull final Ref<EntityStore> ref, final int environmentId, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final World world = componentAccessor.getExternalData().getWorld();
            final LongSet dirtyChunks = new LongOpenHashSet();
            int count = 0;
            for (int x = this.selection.getSelectionMin().getX(); x < this.selection.getSelectionMax().getX(); ++x) {
                for (int z = this.selection.getSelectionMin().getZ(); z < this.selection.getSelectionMax().getZ(); ++z) {
                    final WorldChunk chunk = world.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    dirtyChunks.add(chunk.getIndex());
                    for (int y = this.selection.getSelectionMin().getY(); y < this.selection.getSelectionMax().getY(); ++y) {
                        chunk.getBlockChunk().setEnvironment(x, y, z, environmentId);
                        ++count;
                    }
                }
            }
            dirtyChunks.forEach(value -> world.getNotificationHandler().updateChunk(value));
            this.sendFeedback(Message.translation("server.builderTools.setEnvironment").param("count", count), componentAccessor);
        }
        
        public int copyOrCut(@Nonnull final Ref<EntityStore> ref, final int xMin, final int yMin, final int zMin, final int xMax, final int yMax, final int zMax, final int settings, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) throws PrefabCopyException {
            return this.copyOrCut(ref, xMin, yMin, zMin, xMax, yMax, zMax, settings, null, componentAccessor);
        }
        
        public int copyOrCut(@Nonnull final Ref<EntityStore> ref, final int xMin, final int yMin, final int zMin, final int xMax, final int yMax, final int zMax, final int settings, @Nullable final Vector3i playerAnchor, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) throws PrefabCopyException {
            final World world = componentAccessor.getExternalData().getWorld();
            final long start = System.nanoTime();
            if (this.selection == null) {
                this.selection = new BlockSelection();
            }
            BlockSelection before = null;
            BlockSelection after = null;
            List<SelectionSnapshot<?>> snapshots = Collections.emptyList();
            final boolean cut = (settings & 0x2) != 0x0;
            final boolean empty = (settings & 0x4) != 0x0;
            final boolean blocks = (settings & 0x8) != 0x0;
            final boolean entities = (settings & 0x10) != 0x0;
            final boolean keepAnchors = (settings & 0x40) != 0x0;
            final int width = xMax - xMin;
            final int height = yMax - yMin;
            final int depth = zMax - zMin;
            final int halfWidth = width / 2;
            final int halfDepth = depth / 2;
            if (cut) {
                before = new BlockSelection();
                before.setPosition(xMin + halfWidth, yMin, zMin + halfDepth);
                after = new BlockSelection(before);
                snapshots = new ObjectArrayList<SelectionSnapshot<?>>();
                this.pushHistory(Action.CUT_COPY, ClipboardContentsSnapshot.copyOf(this.selection));
            }
            else {
                this.pushHistory(Action.COPY, ClipboardContentsSnapshot.copyOf(this.selection));
            }
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, xMin + halfWidth, zMin + halfDepth, Math.max(width, depth));
            final BlockTypeAssetMap<String, BlockType> assetMap = BlockType.getAssetMap();
            final int editorBlock = assetMap.getIndex("Editor_Block");
            if (editorBlock == Integer.MIN_VALUE) {
                throw new IllegalArgumentException("Unknown key! Editor_Block");
            }
            final int editorBlockPrefabAir = assetMap.getIndex("Editor_Empty");
            if (editorBlockPrefabAir == Integer.MIN_VALUE) {
                throw new IllegalArgumentException("Unknown key! Editor_Empty");
            }
            final int editorBlockPrefabAnchor = assetMap.getIndex("Editor_Anchor");
            if (editorBlockPrefabAnchor == Integer.MIN_VALUE) {
                throw new IllegalArgumentException("Unknown key! Editor_Anchor");
            }
            final Set<Vector3i> anchors = new HashSet<Vector3i>();
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            (this.selection = new BlockSelection()).setPosition(xMin + halfWidth, yMin, zMin + halfDepth);
            this.selection.setSelectionArea(min, max);
            int count = 0;
            int counter = 0;
            final int top = Math.max(yMin, yMax);
            final int bottom = Math.min(yMin, yMax);
            final int totalBlocks = (width + 1) * (depth + 1) * (top - bottom + 1);
            Store<ChunkStore> store = null;
            for (int x = xMin; x <= xMax; ++x) {
                for (int z = zMin; z <= zMax; ++z) {
                    final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    store = chunk.getReference().getStore();
                    final ChunkColumn chunkColumn = store.getComponent(chunk.getReference(), ChunkColumn.getComponentType());
                    int lastSection = -1;
                    BlockPhysics blockPhysics = null;
                    for (int y = top; y >= bottom; --y) {
                        final int block = chunk.getBlock(x, y, z);
                        final int fluid = chunk.getFluidId(x, y, z);
                        if (lastSection != ChunkUtil.chunkCoordinate(y)) {
                            lastSection = ChunkUtil.chunkCoordinate(y);
                            final Ref<ChunkStore> section = chunkColumn.getSection(lastSection);
                            if (section != null) {
                                blockPhysics = store.getComponent(section, BlockPhysics.getComponentType());
                            }
                            else {
                                blockPhysics = null;
                            }
                        }
                        if (blocks && cut && (block != 0 || fluid != 0 || empty)) {
                            before.copyFromAtWorld(x, y, z, chunk, blockPhysics);
                            after.addEmptyAtWorldPos(x, y, z);
                        }
                        if (block == editorBlockPrefabAnchor && !keepAnchors && playerAnchor == null) {
                            anchors.add(new Vector3i(x, y, z));
                            this.selection.setAnchorAtWorldPos(x, y, z);
                            if (blocks) {
                                final int id = BuilderToolsPlugin.getNonEmptyNeighbourBlock(accessor, x, y, z);
                                if (id > 0 && id != editorBlockPrefabAir) {
                                    this.selection.addBlockAtWorldPos(x, y, z, id, 0, 0, 0);
                                    ++count;
                                }
                                else if (id == editorBlockPrefabAir) {
                                    this.selection.addBlockAtWorldPos(x, y, z, 0, 0, 0, 0);
                                    ++count;
                                }
                            }
                        }
                        else if (blocks && (block != 0 || fluid != 0 || empty) && block != editorBlock) {
                            if (block == editorBlockPrefabAir) {
                                this.selection.addBlockAtWorldPos(x, y, z, 0, 0, 0, 0);
                            }
                            else {
                                this.selection.copyFromAtWorld(x, y, z, chunk, blockPhysics);
                            }
                            ++count;
                        }
                        ++counter;
                        this.sendFeedback(cut ? "Gather 1/2" : "Gather 1/1", totalBlocks, counter, componentAccessor);
                    }
                }
            }
            if (anchors.size() > 1 && playerAnchor == null) {
                final StringBuilder sb = new StringBuilder("Anchors: ");
                boolean first = true;
                for (final Vector3i anchor : anchors) {
                    if (!first) {
                        sb.append(", ");
                    }
                    first = false;
                    sb.append('[').append(anchor.getX()).append(", ").append(anchor.getY()).append(", ").append(anchor.getZ()).append(']');
                }
                throw new PrefabCopyException("Prefab has multiple anchor blocks!\n" + String.valueOf(sb));
            }
            if (playerAnchor != null) {
                this.selection.setAnchorAtWorldPos(playerAnchor.getX(), playerAnchor.getY(), playerAnchor.getZ());
            }
            if (entities) {
                final List<SelectionSnapshot<?>> snapshotsList = snapshots;
                final Store<EntityStore> store2 = world.getEntityStore().getStore();
                final ArrayList<Ref<EntityStore>> entitiesToRemove = cut ? new ArrayList<Ref<EntityStore>>() : null;
                Ref<EntityStore> e = null;
                BuilderToolsPlugin.forEachCopyableInSelection(world, xMin, yMin, zMin, width, height, depth, e -> {
                    final Holder<EntityStore> holder = store.copyEntity(e);
                    this.selection.addEntityFromWorld(holder);
                    if (cut) {
                        snapshotsList.add(new EntityRemoveSnapshot(e));
                        entitiesToRemove.add(e);
                    }
                    return;
                });
                if (cut && entitiesToRemove != null) {
                    final Iterator<Ref<EntityStore>> iterator2 = entitiesToRemove.iterator();
                    while (iterator2.hasNext()) {
                        e = iterator2.next();
                        store2.removeEntity(e, RemoveReason.UNLOAD);
                    }
                }
            }
            if (cut) {
                snapshots.add(new BlockSelectionSnapshot(before));
                this.pushHistory(Action.CUT_REMOVE, snapshots);
            }
            if (after != null) {
                after.placeNoReturn("Cut 2/2", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
                BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            }
            final long end = System.nanoTime();
            final long diff = end - start;
            final int size = count;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute copy of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), size);
            if (cut) {
                this.sendUpdate();
            }
            else {
                this.player.getPlayerConnection().write(Objects.requireNonNullElseGet(this.selection, BlockSelection::new).toPacketWithSelection());
            }
            final int entityCount = entities ? this.selection.getEntityCount() : 0;
            String translationKey;
            if (cut) {
                translationKey = ((entityCount > 0) ? "server.builderTools.cutWithEntities" : "server.builderTools.cut");
            }
            else {
                translationKey = ((entityCount > 0) ? "server.builderTools.copiedWithEntities" : "server.builderTools.copied");
            }
            this.sendFeedback(ref, Message.translation(translationKey).param("blockCount", size).param("entityCount", entityCount), cut ? "SFX_CREATE_CUT" : "SFX_CREATE_COPY", componentAccessor);
            return count;
        }
        
        public int clear(final int xMin, final int yMin, final int zMin, final int xMax, final int yMax, final int zMax, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            final World world = componentAccessor.getExternalData().getWorld();
            final long start = System.nanoTime();
            final BlockSelection before = new BlockSelection();
            final int width = xMax - xMin;
            final int depth = zMax - zMin;
            final int halfWidth = width / 2;
            final int halfDepth = depth / 2;
            before.setPosition(xMin + halfWidth, yMin, zMin + halfDepth);
            before.setSelectionArea(new Vector3i(xMin, yMin, zMin), new Vector3i(xMax, yMax, zMax));
            this.pushHistory(Action.CLEAR, new BlockSelectionSnapshot(before));
            final BlockSelection after = new BlockSelection(before);
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, xMin + halfWidth, zMin + halfDepth, Math.max(width, depth));
            final int top = Math.max(yMin, yMax);
            final int bottom = Math.min(yMin, yMax);
            final int height = top - bottom;
            final int totalBlocks = (width + 1) * (depth + 1) * (height + 1);
            int counter = 0;
            for (int x = xMin; x <= xMax; ++x) {
                for (int z = zMin; z <= zMax; ++z) {
                    final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    final Store<ChunkStore> store = chunk.getReference().getStore();
                    final ChunkColumn chunkColumn = store.getComponent(chunk.getReference(), ChunkColumn.getComponentType());
                    int lastSection = -1;
                    BlockPhysics blockPhysics = null;
                    for (int y = top; y >= bottom; --y) {
                        final int block = chunk.getBlock(x, y, z);
                        final int fluid = chunk.getFluidId(x, y, z);
                        if (lastSection != ChunkUtil.chunkCoordinate(y)) {
                            lastSection = ChunkUtil.chunkCoordinate(y);
                            final Ref<ChunkStore> section = chunkColumn.getSection(lastSection);
                            if (section != null) {
                                blockPhysics = store.getComponent(section, BlockPhysics.getComponentType());
                            }
                            else {
                                blockPhysics = null;
                            }
                        }
                        if (block != 0 || fluid != 0) {
                            before.copyFromAtWorld(x, y, z, chunk, blockPhysics);
                            after.addEmptyAtWorldPos(x, y, z);
                        }
                        ++counter;
                        this.sendFeedback("Gather 1/2", totalBlocks, counter, componentAccessor);
                    }
                }
            }
            after.placeNoReturn("Clear 2/2", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            final int size = after.getBlockCount();
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute clear of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), size);
            this.sendFeedback("Clear", size, componentAccessor);
            return size;
        }
        
        public static RotationTuple transformRotation(final RotationTuple prevRot, final Matrix4d transformationMatrix) {
            Vector3f forwardVec = new Vector3f(1.0f, 0.0f, 0.0f);
            Vector3f upVec = new Vector3f(0.0f, 1.0f, 0.0f);
            forwardVec = Rotation.rotate(forwardVec, prevRot.yaw(), prevRot.pitch(), prevRot.roll());
            upVec = Rotation.rotate(upVec, prevRot.yaw(), prevRot.pitch(), prevRot.roll());
            final Vector3f newForward = transformationMatrix.multiplyDirection(forwardVec.toVector3d()).toVector3f();
            final Vector3f newUp = transformationMatrix.multiplyDirection(upVec.toVector3d()).toVector3f();
            float bestScore = Float.MIN_VALUE;
            RotationTuple bestRot = prevRot;
            for (final RotationTuple rotation : RotationTuple.VALUES) {
                final Vector3f rotForward = Rotation.rotate(new Vector3f(1.0f, 0.0f, 0.0f), rotation.yaw(), rotation.pitch(), rotation.roll());
                final Vector3f rotUp = Rotation.rotate(new Vector3f(0.0f, 1.0f, 0.0f), rotation.yaw(), rotation.pitch(), rotation.roll());
                final float score = rotForward.dot(newForward) + rotUp.dot(newUp);
                if (score > bestScore) {
                    bestScore = score;
                    bestRot = rotation;
                }
            }
            return bestRot;
        }
        
        public void transformThenPasteClipboard(@Nonnull final BlockChange[] blockChanges, @Nullable final PrototypePlayerBuilderToolSettings.FluidChange[] fluidChanges, @Nonnull final Matrix4d transformationMatrix, @Nonnull final Vector3f rotationOrigin, @Nonnull final Vector3i initialPastePoint, final boolean keepEmptyBlocks, final ComponentAccessor<EntityStore> componentAccessor) {
            final World world = componentAccessor.getExternalData().getWorld();
            final long start = System.nanoTime();
            final BlockTypeAssetMap<String, BlockType> assetMap = BlockType.getAssetMap();
            final int editorBlockPrefabAir = keepEmptyBlocks ? assetMap.getIndex("Editor_Empty") : 0;
            int yOffsetOutOfGround = 0;
            for (final BlockChange blockChange : blockChanges) {
                if (blockChange.y < 0 && Math.abs(blockChange.y) > yOffsetOutOfGround) {
                    yOffsetOutOfGround = Math.abs(blockChange.y);
                }
            }
            final Vector4d translationEndResult = new Vector4d(0.0, 0.0, 0.0, 1.0);
            transformationMatrix.multiply(translationEndResult);
            final Vector4d vector4d = translationEndResult;
            vector4d.x += rotationOrigin.x;
            final Vector4d vector4d2 = translationEndResult;
            vector4d2.y += rotationOrigin.y;
            final Vector4d vector4d3 = translationEndResult;
            vector4d3.z += rotationOrigin.z;
            final BlockSelection before = new BlockSelection();
            before.setPosition((int)translationEndResult.x, (int)translationEndResult.y, (int)translationEndResult.z);
            final BlockSelection after = new BlockSelection(before);
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, (int)translationEndResult.x, (int)translationEndResult.z, 50);
            int minX = Integer.MAX_VALUE;
            int minY = Integer.MAX_VALUE;
            int minZ = Integer.MAX_VALUE;
            int maxX = Integer.MIN_VALUE;
            int maxY = Integer.MIN_VALUE;
            int maxZ = Integer.MIN_VALUE;
            final Vector4d mutable4d = new Vector4d(0.0, 0.0, 0.0, 1.0);
            for (final BlockChange blockChange2 : blockChanges) {
                mutable4d.assign(blockChange2.x - rotationOrigin.x + initialPastePoint.x + 0.5, blockChange2.y - rotationOrigin.y + initialPastePoint.y + 0.5 + yOffsetOutOfGround, blockChange2.z - rotationOrigin.z + initialPastePoint.z + 0.5, 1.0);
                transformationMatrix.multiply(mutable4d);
                final Vector3i rotatedLocation = new Vector3i((int)Math.floor(mutable4d.x + 0.1 + rotationOrigin.x - 0.5), (int)Math.floor(mutable4d.y + 0.1 + rotationOrigin.y - 0.5), (int)Math.floor(mutable4d.z + 0.1 + rotationOrigin.z - 0.5));
                minX = Math.min(minX, rotatedLocation.x);
                minY = Math.min(minY, rotatedLocation.y);
                minZ = Math.min(minZ, rotatedLocation.z);
                maxX = Math.max(maxX, rotatedLocation.x);
                maxY = Math.max(maxY, rotatedLocation.y);
                maxZ = Math.max(maxZ, rotatedLocation.z);
                final WorldChunk currentChunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(rotatedLocation.x, rotatedLocation.z));
                final Holder<ChunkStore> holder = currentChunk.getBlockComponentHolder(rotatedLocation.x, rotatedLocation.y, rotatedLocation.z);
                final int blockIdInRotatedLocation = currentChunk.getBlock(rotatedLocation.x, rotatedLocation.y, rotatedLocation.z);
                final int filler = currentChunk.getFiller(rotatedLocation.x, rotatedLocation.y, rotatedLocation.z);
                final int rotation = currentChunk.getRotationIndex(rotatedLocation.x, rotatedLocation.y, rotatedLocation.z);
                before.addBlockAtWorldPos(rotatedLocation.x, rotatedLocation.y, rotatedLocation.z, blockIdInRotatedLocation, rotation, filler, 0, holder);
                final int originalFluidId = currentChunk.getFluidId(rotatedLocation.x, rotatedLocation.y, rotatedLocation.z);
                final byte originalFluidLevel = currentChunk.getFluidLevel(rotatedLocation.x, rotatedLocation.y, rotatedLocation.z);
                before.addFluidAtWorldPos(rotatedLocation.x, rotatedLocation.y, rotatedLocation.z, originalFluidId, originalFluidLevel);
                final int newRotation = transformRotation(RotationTuple.get(blockChange2.rotation), transformationMatrix).index();
                int blockIdToPlace = blockChange2.block;
                if (blockChange2.block == 0 && keepEmptyBlocks) {
                    blockIdToPlace = editorBlockPrefabAir;
                }
                final BlockType blockType = assetMap.getAsset(blockIdToPlace);
                if (blockType != null) {
                    final BlockBoundingBoxes hitbox = BlockBoundingBoxes.getAssetMap().getAsset(blockType.getHitboxTypeIndex());
                    if (hitbox != null) {
                        final int finalBlockIdToPlace = blockIdToPlace;
                        if (hitbox.protrudesUnitBox()) {
                            FillerBlockUtil.forEachFillerBlock(hitbox.get(newRotation), (x, y, z) -> after.addBlockAtWorldPos(rotatedLocation.x + x, rotatedLocation.y + y, rotatedLocation.z + z, finalBlockIdToPlace, newRotation, FillerBlockUtil.pack(x, y, z), 0, holder));
                        }
                        else {
                            after.addBlockAtWorldPos(rotatedLocation.x, rotatedLocation.y, rotatedLocation.z, blockIdToPlace, newRotation, 0, 0, holder);
                        }
                    }
                }
            }
            final int finalYOffsetOutOfGround = yOffsetOutOfGround;
            if (fluidChanges != null) {
                for (final PrototypePlayerBuilderToolSettings.FluidChange fluidChange : fluidChanges) {
                    mutable4d.assign(fluidChange.x() - rotationOrigin.x + initialPastePoint.x + 0.5, fluidChange.y() - rotationOrigin.y + initialPastePoint.y + 0.5 + finalYOffsetOutOfGround, fluidChange.z() - rotationOrigin.z + initialPastePoint.z + 0.5, 1.0);
                    transformationMatrix.multiply(mutable4d);
                    final Vector3i rotatedLocation2 = new Vector3i((int)Math.floor(mutable4d.x + 0.1 + rotationOrigin.x - 0.5), (int)Math.floor(mutable4d.y + 0.1 + rotationOrigin.y - 0.5), (int)Math.floor(mutable4d.z + 0.1 + rotationOrigin.z - 0.5));
                    after.addFluidAtWorldPos(rotatedLocation2.x, rotatedLocation2.y, rotatedLocation2.z, fluidChange.fluidId(), fluidChange.fluidLevel());
                }
            }
            if (minX != Integer.MAX_VALUE) {
                before.setSelectionArea(new Vector3i(minX, minY, minZ), new Vector3i(maxX, maxY, maxZ));
            }
            this.pushHistory(Action.ROTATE, new BlockSelectionSnapshot(before));
            after.placeNoReturn("Transform 1/1", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute set of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), after.getBlockCount());
            this.sendUpdate();
            this.sendArea();
        }
        
        public void transformSelectionPoints(@Nonnull final Matrix4d transformationMatrix, @Nonnull final Vector3f rotationOrigin) {
            final Vector3i newMin = this.transformBlockLocation(this.selection.getSelectionMin(), transformationMatrix, rotationOrigin);
            final Vector3i newMax = this.transformBlockLocation(this.selection.getSelectionMax(), transformationMatrix, rotationOrigin);
            this.selection.setSelectionArea(Vector3i.min(newMin, newMax), Vector3i.max(newMin, newMax));
            this.sendUpdate();
            this.sendArea();
        }
        
        @Nonnull
        public Vector3i transformBlockLocation(@Nonnull final Vector3i blockLocation, @Nonnull final Matrix4d transformationMatrix, @Nonnull final Vector3f rotationOrigin) {
            final Vector4d relativeOffset = new Vector4d(blockLocation.x - rotationOrigin.x + 0.5, blockLocation.y - rotationOrigin.y + 0.5, blockLocation.z - rotationOrigin.z + 0.5, 1.0);
            transformationMatrix.multiply(relativeOffset);
            return new Vector3i((int)Math.floor(relativeOffset.x + rotationOrigin.x - 0.5 + 0.1), (int)Math.floor(relativeOffset.y + rotationOrigin.y - 0.5 + 0.1), (int)Math.floor(relativeOffset.z + rotationOrigin.z - 0.5 + 0.1));
        }
        
        public int paste(@Nonnull final Ref<EntityStore> ref, final int x, final int y, final int z, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            return this.paste(ref, x, y, z, false, componentAccessor);
        }
        
        public int paste(@Nonnull final Ref<EntityStore> ref, final int x, final int y, final int z, final boolean technicalPaste, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            final World world = componentAccessor.getExternalData().getWorld();
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionClipboardEmpty"), componentAccessor);
                return 0;
            }
            final long start = System.nanoTime();
            final Vector3i selMin = this.selection.getSelectionMin();
            final Vector3i selMax = this.selection.getSelectionMax();
            final int origPosX = (selMin.x + selMax.x) / 2;
            final int origPosY = selMin.y;
            final int origPosZ = (selMin.z + selMax.z) / 2;
            final int offsetX = x - origPosX;
            final int offsetY = y - origPosY;
            final int offsetZ = z - origPosZ;
            final Vector3i pasteMin = new Vector3i(selMin.x + offsetX, selMin.y + offsetY, selMin.z + offsetZ);
            final Vector3i pasteMax = new Vector3i(selMax.x + offsetX, selMax.y + offsetY, selMax.z + offsetZ);
            BlockSelection selectionToPlace = this.selection;
            if (technicalPaste) {
                selectionToPlace = this.convertEmptyBlocksToEditorEmpty(this.selection);
            }
            selectionToPlace.setPosition(x, y, z);
            final int prefabId = PrefabUtil.getNextPrefabId();
            selectionToPlace.setPrefabId(prefabId);
            if (!BuilderToolsPlugin.onPasteStart(prefabId, componentAccessor)) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.pasteCancelledByEvent"), componentAccessor);
                return 0;
            }
            final int entityCount = selectionToPlace.getEntityCount();
            final List<SelectionSnapshot<?>> snapshots = new ObjectArrayList<SelectionSnapshot<?>>(entityCount + 1);
            Consumer<Ref<EntityStore>> collector = BlockSelection.DEFAULT_ENTITY_CONSUMER;
            if (entityCount > 0) {
                collector = (e -> snapshots.add(new EntityAddSnapshot(e)));
            }
            final BlockSelection before = selectionToPlace.place(this.player, world, Vector3i.ZERO, this.globalMask, collector);
            before.setSelectionArea(pasteMin, pasteMax);
            snapshots.add(new BlockSelectionSnapshot(before));
            this.pushHistory(Action.PASTE, snapshots);
            BuilderToolsPlugin.invalidateWorldMapForBounds(pasteMin, pasteMax, world);
            BuilderToolsPlugin.get().onPasteEnd(prefabId, componentAccessor);
            selectionToPlace.setPrefabId(-1);
            selectionToPlace.setPosition(0, 0, 0);
            final long end = System.nanoTime();
            final long diff = end - start;
            final int size = selectionToPlace.getBlockCount();
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute paste of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), size);
            this.sendFeedback(Message.translation("server.builderTools.pastedBlocks").param("count", size), componentAccessor);
            return size;
        }
        
        private BlockSelection convertEmptyBlocksToEditorEmpty(@Nonnull final BlockSelection original) {
            final BlockTypeAssetMap<String, BlockType> assetMap = BlockType.getAssetMap();
            final int editorBlockPrefabAir = assetMap.getIndex("Editor_Empty");
            if (editorBlockPrefabAir == Integer.MIN_VALUE) {
                return original;
            }
            final BlockSelection converted = new BlockSelection(original.getBlockCount(), original.getEntityCount());
            converted.setPosition(original.getX(), original.getY(), original.getZ());
            converted.setAnchor(original.getAnchorX(), original.getAnchorY(), original.getAnchorZ());
            converted.setSelectionArea(original.getSelectionMin(), original.getSelectionMax());
            original.forEachBlock((x, y, z, block) -> {
                final int blockId = (block.blockId() == 0) ? editorBlockPrefabAir : block.blockId();
                converted.addBlockAtLocalPos(x, y, z, blockId, block.rotation(), block.filler(), block.supportValue(), block.holder());
                return;
            });
            original.forEachFluid((x, y, z, fluidId, fluidLevel) -> converted.addFluidAtLocalPos(x, y, z, fluidId, fluidLevel));
            original.forEachEntity(holder -> converted.addEntityHolderRaw(holder.clone()));
            return converted;
        }
        
        public void rotate(@Nonnull final Ref<EntityStore> ref, @Nonnull final Axis axis, final int angle, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection != null) {
                final long start = System.nanoTime();
                this.pushHistory(Action.ROTATE, ClipboardContentsSnapshot.copyOf(this.selection));
                this.selection = this.selection.rotate(axis, angle);
                final long end = System.nanoTime();
                final long diff = end - start;
                BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute rotate of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), this.selection.getBlockCount());
                this.sendUpdate();
                this.sendFeedback(Message.translation("server.builderTools.clipboardRotatedBy").param("angle", angle).param("axis", axis.toString()), componentAccessor);
            }
            else {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionClipboardEmpty"), componentAccessor);
            }
        }
        
        public void rotate(@Nonnull final Ref<EntityStore> ref, @Nonnull final Axis axis, final int angle, @Nonnull final Vector3f originOfRotation, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection != null) {
                final long start = System.nanoTime();
                this.pushHistory(Action.ROTATE, ClipboardContentsSnapshot.copyOf(this.selection));
                this.selection = this.selection.rotate(axis, angle, originOfRotation);
                final long end = System.nanoTime();
                final long diff = end - start;
                BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute rotate of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), this.selection.getBlockCount());
                this.sendUpdate();
                this.sendFeedback(Message.translation("server.builderTools.clipboardRotatedBy").param("angle", angle).param("axis", axis.toString()), componentAccessor);
            }
            else {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionClipboardEmpty"), componentAccessor);
            }
        }
        
        public void rotateArbitrary(@Nonnull final Ref<EntityStore> ref, final float yaw, final float pitch, final float roll, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection != null) {
                final long start = System.nanoTime();
                this.pushHistory(Action.ROTATE, ClipboardContentsSnapshot.copyOf(this.selection));
                this.selection = this.selection.rotateArbitrary(yaw, pitch, roll);
                final long end = System.nanoTime();
                final long diff = end - start;
                BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute arbitrary rotate of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), this.selection.getBlockCount());
                this.sendUpdate();
                final Message message = Message.translation("server.builderTools.clipboardRotatedArbitrary").param("yaw", yaw).param("pitch", pitch).param("roll", roll);
                this.sendFeedback(message, componentAccessor);
            }
            else {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionClipboardEmpty"), componentAccessor);
            }
        }
        
        public void flip(@Nonnull final Ref<EntityStore> ref, @Nonnull final Axis axis, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection != null) {
                final long start = System.nanoTime();
                this.pushHistory(Action.FLIP, ClipboardContentsSnapshot.copyOf(this.selection));
                this.selection = this.selection.flip(axis);
                final long end = System.nanoTime();
                final long diff = end - start;
                BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute flip of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), this.selection.getBlockCount());
                this.sendUpdate();
                this.sendFeedback(Message.translation("server.builderTools.clipboardFlipped").param("axis", axis.toString()), componentAccessor);
            }
            else {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionClipboardEmpty"), componentAccessor);
            }
        }
        
        public void hollow(@Nonnull final Ref<EntityStore> ref, final int blockId, final int thickness, final boolean setTop, final boolean setBottom, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final long start = System.nanoTime();
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final BlockSelection before = new BlockSelection();
            before.setPosition(min.x, min.y, min.z);
            before.setSelectionArea(min, max);
            final BlockSelection after = new BlockSelection(before);
            final World world = componentAccessor.getExternalData().getWorld();
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, max.x + 1 - min.x, max.z + 1 - min.z, Math.max(max.x + 1 - min.x, Math.max(max.y + 1 - min.y, max.z + 1 - min.z)));
            BlockCubeUtil.forEachBlock(min, max, thickness, !setTop, !setBottom, true, null, (TriIntObjPredicate<Object>)new TriIntObjPredicate<Void>() {
                private int previousX = Integer.MIN_VALUE;
                private int previousZ = Integer.MIN_VALUE;
                @Nullable
                private WorldChunk currentChunk;
                
                @Override
                public boolean test(final int x, final int y, final int z, final Void unused) {
                    if (this.previousX != x || this.previousZ != z) {
                        this.previousX = x;
                        this.previousZ = z;
                        this.currentChunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    }
                    final int currentBlockId = this.currentChunk.getBlock(x, y, z);
                    final int currentFluidId = this.currentChunk.getFluidId(x, y, z);
                    if (BuilderState.this.globalMask != null && BuilderState.this.globalMask.isExcluded(accessor, x, y, z, min, max, currentBlockId, currentFluidId)) {
                        return true;
                    }
                    final Holder<ChunkStore> holder = this.currentChunk.getBlockComponentHolder(x, y, z);
                    final Holder<ChunkStore> newHolder = BuilderToolsPlugin.createBlockComponent(this.currentChunk, x, y, z, blockId, currentBlockId, holder, false);
                    final int supportValue = this.currentChunk.getSupportValue(x, y, z);
                    final int filler = this.currentChunk.getFiller(x, y, z);
                    final int rotation = this.currentChunk.getRotationIndex(x, y, z);
                    before.addBlockAtWorldPos(x, y, z, currentBlockId, filler, rotation, supportValue, holder);
                    after.addBlockAtWorldPos(x, y, z, blockId, 0, 0, 0, newHolder);
                    return true;
                }
            });
            this.pushHistory(Action.HOLLOW, new BlockSelectionSnapshot(before));
            after.placeNoReturn("Hollow 1/1", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute set of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), after.getBlockCount());
            this.sendUpdate();
            this.sendArea();
        }
        
        public void walls(@Nonnull final Ref<EntityStore> ref, final int blockId, final int thickness, final boolean cappedTop, final boolean cappedBottom, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            this.walls(ref, BlockPattern.parse(BlockType.getAssetMap().getAsset(blockId).getId()), thickness, cappedTop, cappedBottom, componentAccessor);
        }
        
        public void walls(@Nonnull final Ref<EntityStore> ref, @Nonnull final BlockPattern pattern, final int thickness, final boolean cappedTop, final boolean cappedBottom, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (pattern.isEmpty()) {
                return;
            }
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final World world = componentAccessor.getExternalData().getWorld();
            final long start = System.nanoTime();
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final BlockSelection before = new BlockSelection();
            before.setPosition(min.x, min.y, min.z);
            before.setSelectionArea(min, max);
            final BlockSelection after = new BlockSelection(before);
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, max.x + 1 - min.x, max.z + 1 - min.z, Math.max(max.x + 1 - min.x, Math.max(max.y + 1 - min.y, max.z + 1 - min.z)));
            BlockCubeUtil.forEachBlock(min, max, thickness, cappedTop, cappedBottom, false, null, (TriIntObjPredicate<Object>)new TriIntObjPredicate<Void>() {
                private int previousX = Integer.MIN_VALUE;
                private int previousZ = Integer.MIN_VALUE;
                @Nullable
                private WorldChunk currentChunk;
                
                @Override
                public boolean test(final int x, final int y, final int z, final Void unused) {
                    if (this.previousX != x || this.previousZ != z) {
                        this.previousX = x;
                        this.previousZ = z;
                        this.currentChunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    }
                    final int currentBlockId = this.currentChunk.getBlock(x, y, z);
                    final int currentFluidId = this.currentChunk.getFluidId(x, y, z);
                    if (BuilderState.this.globalMask != null && BuilderState.this.globalMask.isExcluded(accessor, x, y, z, min, max, currentBlockId, currentFluidId)) {
                        return true;
                    }
                    final Material material = Material.fromPattern(pattern, BuilderState.this.random);
                    if (material.isFluid()) {
                        final byte currentFluidLevel = this.currentChunk.getFluidLevel(x, y, z);
                        before.addFluidAtWorldPos(x, y, z, currentFluidId, currentFluidLevel);
                        after.addFluidAtWorldPos(x, y, z, material.getFluidId(), material.getFluidLevel());
                    }
                    else {
                        final int newBlockId = material.getBlockId();
                        final int newRotation = material.getRotation();
                        final Holder<ChunkStore> holder = this.currentChunk.getBlockComponentHolder(x, y, z);
                        final Holder<ChunkStore> newHolder = BuilderToolsPlugin.createBlockComponent(this.currentChunk, x, y, z, newBlockId, currentBlockId, holder, false);
                        final int supportValue = this.currentChunk.getSupportValue(x, y, z);
                        final int filler = this.currentChunk.getFiller(x, y, z);
                        final int rotation = this.currentChunk.getRotationIndex(x, y, z);
                        before.addBlockAtWorldPos(x, y, z, currentBlockId, rotation, filler, supportValue, holder);
                        after.addBlockAtWorldPos(x, y, z, newBlockId, newRotation, 0, 0, newHolder);
                        if (newBlockId == 0) {
                            final int fluidId = this.currentChunk.getFluidId(x, y, z);
                            final byte fluidLevel = this.currentChunk.getFluidLevel(x, y, z);
                            if (fluidId != 0) {
                                before.addFluidAtWorldPos(x, y, z, fluidId, fluidLevel);
                                after.addFluidAtWorldPos(x, y, z, 0, (byte)0);
                            }
                        }
                    }
                    return true;
                }
            });
            this.pushHistory(Action.WALLS, new BlockSelectionSnapshot(before));
            after.placeNoReturn("Walls 1/1", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute walls of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), after.getBlockCount());
            this.sendUpdate();
            this.sendArea();
        }
        
        public void set(final int blockId, final ComponentAccessor<EntityStore> componentAccessor) {
            this.set(BlockPattern.parse(BlockType.getAssetMap().getAsset(blockId).getId()), componentAccessor);
        }
        
        public void set(@Nonnull final BlockPattern pattern, final ComponentAccessor<EntityStore> componentAccessor) {
            if (pattern.isEmpty()) {
                return;
            }
            if (this.selection == null) {
                this.sendFeedback(Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendFeedback(Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final long start = System.nanoTime();
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final int xMin = min.getX();
            final int xMax = max.getX();
            final int yMin = min.getY();
            final int yMax = max.getY();
            final int zMin = min.getZ();
            final int zMax = max.getZ();
            final int totalBlocks = (xMax - xMin + 1) * (zMax - zMin + 1) * (yMax - yMin + 1);
            final int width = xMax - xMin;
            final int depth = zMax - zMin;
            final int halfWidth = width / 2;
            final int halfDepth = depth / 2;
            final BlockSelection before = new BlockSelection(totalBlocks, 0);
            before.setPosition(xMin + halfWidth, yMin, zMin + halfDepth);
            before.setSelectionArea(min, max);
            this.pushHistory(Action.SET, new BlockSelectionSnapshot(before));
            final BlockSelection after = new BlockSelection(totalBlocks, 0);
            after.copyPropertiesFrom(before);
            final World world = componentAccessor.getExternalData().getWorld();
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, xMin + halfWidth, zMin + halfDepth, Math.max(width, depth));
            int counter = 0;
            for (int x = xMin; x <= xMax; ++x) {
                for (int z = zMin; z <= zMax; ++z) {
                    final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    for (int y = yMax; y >= yMin; --y) {
                        final int currentBlock = chunk.getBlock(x, y, z);
                        final int currentFluid = chunk.getFluidId(x, y, z);
                        if (this.globalMask != null && this.globalMask.isExcluded(accessor, x, y, z, min, max, currentBlock, currentFluid)) {
                            ++counter;
                        }
                        else {
                            final Material material = Material.fromPattern(pattern, this.random);
                            if (material.isFluid()) {
                                final byte currentFluidLevel = chunk.getFluidLevel(x, y, z);
                                before.addFluidAtWorldPos(x, y, z, currentFluid, currentFluidLevel);
                                after.addFluidAtWorldPos(x, y, z, material.getFluidId(), material.getFluidLevel());
                            }
                            else {
                                final int newBlockId = material.getBlockId();
                                final int newRotation = material.getRotation();
                                final Holder<ChunkStore> holder = chunk.getBlockComponentHolder(x, y, z);
                                final Holder<ChunkStore> newHolder = BuilderToolsPlugin.createBlockComponent(chunk, x, y, z, newBlockId, currentBlock, holder, false);
                                final int supportValue = chunk.getSupportValue(x, y, z);
                                final int filler = chunk.getFiller(x, y, z);
                                final int rotation = chunk.getRotationIndex(x, y, z);
                                before.addBlockAtWorldPos(x, y, z, currentBlock, rotation, filler, supportValue, holder);
                                after.addBlockAtWorldPos(x, y, z, newBlockId, newRotation, 0, 0, newHolder);
                                if (newBlockId == 0) {
                                    final int fluidId = chunk.getFluidId(x, y, z);
                                    final byte fluidLevel = chunk.getFluidLevel(x, y, z);
                                    if (fluidId != 0) {
                                        before.addFluidAtWorldPos(x, y, z, fluidId, fluidLevel);
                                        after.addFluidAtWorldPos(x, y, z, 0, (byte)0);
                                    }
                                }
                            }
                            ++counter;
                            this.sendFeedback("Gather 1/2", totalBlocks, counter, componentAccessor);
                        }
                    }
                }
            }
            after.placeNoReturn("Set 2/2", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute set of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), counter);
            this.sendUpdate();
            this.sendArea();
        }
        
        public void fill(@Nonnull final BlockPattern pattern, final ComponentAccessor<EntityStore> componentAccessor) {
            if (pattern.isEmpty()) {
                return;
            }
            if (this.selection == null) {
                this.sendFeedback(Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendFeedback(Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final long start = System.nanoTime();
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final int xMin = min.getX();
            final int xMax = max.getX();
            final int yMin = min.getY();
            final int yMax = max.getY();
            final int zMin = min.getZ();
            final int zMax = max.getZ();
            final int totalBlocks = (xMax - xMin + 1) * (zMax - zMin + 1) * (yMax - yMin + 1);
            final int width = xMax - xMin;
            final int depth = zMax - zMin;
            final int halfWidth = width / 2;
            final int halfDepth = depth / 2;
            final BlockSelection before = new BlockSelection(totalBlocks, 0);
            before.setPosition(xMin + halfWidth, yMin, zMin + halfDepth);
            before.setSelectionArea(min, max);
            this.pushHistory(Action.EDIT, new BlockSelectionSnapshot(before));
            final BlockSelection after = new BlockSelection(totalBlocks, 0);
            after.copyPropertiesFrom(before);
            final World world = componentAccessor.getExternalData().getWorld();
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, xMin + halfWidth, zMin + halfDepth, Math.max(width, depth));
            int counter = 0;
            for (int x = xMin; x <= xMax; ++x) {
                for (int z = zMin; z <= zMax; ++z) {
                    final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    for (int y = yMax; y >= yMin; --y) {
                        final Material material = Material.fromPattern(pattern, this.random);
                        if (material.isFluid()) {
                            final int currentFluidId = chunk.getFluidId(x, y, z);
                            if (currentFluidId == 0) {
                                final byte currentFluidLevel = chunk.getFluidLevel(x, y, z);
                                before.addFluidAtWorldPos(x, y, z, currentFluidId, currentFluidLevel);
                                after.addFluidAtWorldPos(x, y, z, material.getFluidId(), material.getFluidLevel());
                            }
                        }
                        else {
                            final int currentBlock = chunk.getBlock(x, y, z);
                            if (currentBlock == 0) {
                                final int newBlockId = material.getBlockId();
                                final int newRotation = material.getRotation();
                                final Holder<ChunkStore> holder = chunk.getBlockComponentHolder(x, y, z);
                                final Holder<ChunkStore> newHolder = BuilderToolsPlugin.createBlockComponent(chunk, x, y, z, newBlockId, currentBlock, holder, false);
                                final int supportValue = chunk.getSupportValue(x, y, z);
                                final int filler = chunk.getFiller(x, y, z);
                                final int rotation = chunk.getRotationIndex(x, y, z);
                                before.addBlockAtWorldPos(x, y, z, currentBlock, rotation, filler, supportValue, holder);
                                after.addBlockAtWorldPos(x, y, z, newBlockId, newRotation, 0, 0, newHolder);
                            }
                        }
                        ++counter;
                        this.sendFeedback("Gather 1/2", totalBlocks, counter, componentAccessor);
                    }
                }
            }
            after.placeNoReturn("Fill 2/2", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute fill of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), counter);
            this.sendUpdate();
            this.sendArea();
        }
        
        public void replace(@Nonnull final Ref<EntityStore> ref, @Nonnull final Material from, @Nonnull final Material to, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendFeedback(Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendFeedback(Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final long start = System.nanoTime();
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final int xMin = min.getX();
            final int xMax = max.getX();
            final int yMin = min.getY();
            final int yMax = max.getY();
            final int zMin = min.getZ();
            final int zMax = max.getZ();
            final BlockSelection before = new BlockSelection();
            final int width = xMax - xMin;
            final int depth = zMax - zMin;
            final int halfWidth = width / 2;
            final int halfDepth = depth / 2;
            before.setPosition(xMin + halfWidth, yMin, zMin + halfDepth);
            before.setSelectionArea(min, max);
            this.pushHistory(Action.REPLACE, new BlockSelectionSnapshot(before));
            final BlockSelection after = new BlockSelection(before);
            final World world = componentAccessor.getExternalData().getWorld();
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, xMin + halfWidth, zMin + halfDepth, Math.max(width, depth));
            final int totalBlocks = (width + 1) * (depth + 1) * (yMax - yMin + 1);
            int counter = 0;
            for (int x = xMin; x <= xMax; ++x) {
                for (int z = zMin; z <= zMax; ++z) {
                    final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    for (int y = yMax; y >= yMin; --y) {
                        final int currentFiller = chunk.getFiller(x, y, z);
                        if (currentFiller != 0) {
                            ++counter;
                            this.sendFeedback("Gather 1/2", totalBlocks, counter, componentAccessor);
                        }
                        else {
                            boolean shouldReplace = false;
                            if (from.isFluid()) {
                                final int currentFluidId = chunk.getFluidId(x, y, z);
                                shouldReplace = (currentFluidId == from.getFluidId());
                            }
                            else {
                                final int currentBlock = chunk.getBlock(x, y, z);
                                shouldReplace = (currentBlock == from.getBlockId());
                            }
                            if (shouldReplace) {
                                final int currentBlock = chunk.getBlock(x, y, z);
                                final int currentFluidId2 = chunk.getFluidId(x, y, z);
                                final byte currentFluidLevel = chunk.getFluidLevel(x, y, z);
                                if (to.isFluid()) {
                                    if (currentBlock != 0) {
                                        final Holder<ChunkStore> holder = chunk.getBlockComponentHolder(x, y, z);
                                        final int rotation = chunk.getRotationIndex(x, y, z);
                                        final int supportValue = chunk.getSupportValue(x, y, z);
                                        before.addBlockAtWorldPos(x, y, z, currentBlock, rotation, currentFiller, supportValue, holder);
                                        after.addBlockAtWorldPos(x, y, z, 0, 0, 0, 0);
                                        this.clearFillerBlocksIfNeeded(x, y, z, currentBlock, rotation, accessor, before, after);
                                    }
                                    before.addFluidAtWorldPos(x, y, z, currentFluidId2, currentFluidLevel);
                                    after.addFluidAtWorldPos(x, y, z, to.getFluidId(), to.getFluidLevel());
                                }
                                else if (to.isEmpty()) {
                                    final Holder<ChunkStore> holder = chunk.getBlockComponentHolder(x, y, z);
                                    final int rotation = chunk.getRotationIndex(x, y, z);
                                    final int supportValue = chunk.getSupportValue(x, y, z);
                                    before.addBlockAtWorldPos(x, y, z, currentBlock, rotation, currentFiller, supportValue, holder);
                                    after.addBlockAtWorldPos(x, y, z, 0, 0, 0, 0);
                                    this.clearFillerBlocksIfNeeded(x, y, z, currentBlock, rotation, accessor, before, after);
                                    if (currentFluidId2 != 0) {
                                        before.addFluidAtWorldPos(x, y, z, currentFluidId2, currentFluidLevel);
                                        after.addFluidAtWorldPos(x, y, z, 0, (byte)0);
                                    }
                                }
                                else {
                                    if (currentFluidId2 != 0) {
                                        before.addFluidAtWorldPos(x, y, z, currentFluidId2, currentFluidLevel);
                                        after.addFluidAtWorldPos(x, y, z, 0, (byte)0);
                                    }
                                    final Holder<ChunkStore> holder = chunk.getBlockComponentHolder(x, y, z);
                                    final Holder<ChunkStore> newHolder = BuilderToolsPlugin.createBlockComponent(chunk, x, y, z, to.getBlockId(), currentBlock, holder, true);
                                    final int rotation2 = chunk.getRotationIndex(x, y, z);
                                    final int supportValue2 = chunk.getSupportValue(x, y, z);
                                    before.addBlockAtWorldPos(x, y, z, currentBlock, rotation2, currentFiller, supportValue2, holder);
                                    after.addBlockAtWorldPos(x, y, z, to.getBlockId(), rotation2, 0, 0, newHolder);
                                    this.replaceMultiBlockStructure(x, y, z, currentBlock, to.getBlockId(), rotation2, accessor, before, after);
                                }
                            }
                            ++counter;
                            this.sendFeedback("Gather 1/2", totalBlocks, counter, componentAccessor);
                        }
                    }
                }
            }
            after.placeNoReturn("Replace 2/2", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute replace", diff, TimeUnit.NANOSECONDS.toMillis(diff));
            this.sendUpdate();
            this.sendArea();
            SoundUtil.playSoundEvent2d(ref, TempAssetIdUtil.getSoundEventIndex("CREATE_SELECTION_FILL"), SoundCategory.SFX, componentAccessor);
        }
        
        private void clearFillerBlocksIfNeeded(final int baseX, final int baseY, final int baseZ, final int oldBlockId, final int rotationIndex, final LocalCachedChunkAccessor accessor, final BlockSelection before, final BlockSelection after) {
            this.replaceMultiBlockStructure(baseX, baseY, baseZ, oldBlockId, 0, rotationIndex, accessor, before, after);
        }
        
        private void replaceMultiBlockStructure(final int baseX, final int baseY, final int baseZ, final int oldBlockId, final int newBlockId, final int rotationIndex, final LocalCachedChunkAccessor accessor, final BlockSelection before, final BlockSelection after) {
            final BlockTypeAssetMap<String, BlockType> blockTypeAssetMap = BlockType.getAssetMap();
            final IndexedLookupTableAssetMap<String, BlockBoundingBoxes> hitboxAssetMap = BlockBoundingBoxes.getAssetMap();
            final BlockType oldBlockType = blockTypeAssetMap.getAsset(oldBlockId);
            BlockBoundingBoxes oldHitbox = null;
            if (oldBlockType != null) {
                oldHitbox = hitboxAssetMap.getAsset(oldBlockType.getHitboxTypeIndex());
            }
            final BlockType newBlockType = blockTypeAssetMap.getAsset(newBlockId);
            BlockBoundingBoxes newHitbox = null;
            if (newBlockType != null) {
                newHitbox = hitboxAssetMap.getAsset(newBlockType.getHitboxTypeIndex());
            }
            if (oldHitbox != null && oldHitbox.protrudesUnitBox()) {
                final BlockBoundingBoxes finalNewHitbox = newHitbox;
                FillerBlockUtil.forEachFillerBlock(oldHitbox.get(rotationIndex), (fx, fy, fz) -> {
                    if (fx == 0 && fy == 0 && fz == 0) {
                        return;
                    }
                    else {
                        final int fillerX = baseX + fx;
                        final int fillerY = baseY + fy;
                        final int fillerZ = baseZ + fz;
                        final WorldChunk fillerChunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(fillerX, fillerZ));
                        final int fillerBlock = fillerChunk.getBlock(fillerX, fillerY, fillerZ);
                        final int fillerFiller = fillerChunk.getFiller(fillerX, fillerY, fillerZ);
                        if (fillerFiller != 0) {
                            final Holder<ChunkStore> fillerHolder = fillerChunk.getBlockComponentHolder(fillerX, fillerY, fillerZ);
                            before.addBlockAtWorldPos(fillerX, fillerY, fillerZ, fillerBlock, rotationIndex, fillerFiller, fillerChunk.getSupportValue(fillerX, fillerY, fillerZ), fillerHolder);
                            final boolean willBeFilledByNewStructure = finalNewHitbox != null && finalNewHitbox.protrudesUnitBox() && finalNewHitbox.get(rotationIndex).getBoundingBox().containsBlock(fx, fy, fz);
                            if (!willBeFilledByNewStructure) {
                                after.addBlockAtWorldPos(fillerX, fillerY, fillerZ, 0, 0, 0, 0);
                            }
                        }
                        return;
                    }
                });
            }
            if (newHitbox != null && newHitbox.protrudesUnitBox()) {
                FillerBlockUtil.forEachFillerBlock(newHitbox.get(rotationIndex), (fx, fy, fz) -> {
                    if (fx != 0 || fy != 0 || fz != 0) {
                        final int fillerX2 = baseX + fx;
                        final int fillerY2 = baseY + fy;
                        final int fillerZ2 = baseZ + fz;
                        final WorldChunk fillerChunk2 = accessor.getChunk(ChunkUtil.indexChunkFromBlock(fillerX2, fillerZ2));
                        final int existingBlock = fillerChunk2.getBlock(fillerX2, fillerY2, fillerZ2);
                        final int existingFiller = fillerChunk2.getFiller(fillerX2, fillerY2, fillerZ2);
                        if (existingFiller == 0 && !before.hasBlockAtWorldPos(fillerX2, fillerY2, fillerZ2)) {
                            final Holder<ChunkStore> fillerHolder2 = fillerChunk2.getBlockComponentHolder(fillerX2, fillerY2, fillerZ2);
                            before.addBlockAtWorldPos(fillerX2, fillerY2, fillerZ2, existingBlock, rotationIndex, existingFiller, fillerChunk2.getSupportValue(fillerX2, fillerY2, fillerZ2), fillerHolder2);
                        }
                        final int newFiller = FillerBlockUtil.pack(fx, fy, fz);
                        after.addBlockAtWorldPos(fillerX2, fillerY2, fillerZ2, newBlockId, rotationIndex, newFiller, 0);
                    }
                });
            }
        }
        
        public void replace(@Nonnull final Ref<EntityStore> ref, @Nullable final IntPredicate doReplace, @Nonnull final BlockPattern toPattern, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final long start = System.nanoTime();
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final int xMin = min.getX();
            final int xMax = max.getX();
            final int yMin = min.getY();
            final int yMax = max.getY();
            final int zMin = min.getZ();
            final int zMax = max.getZ();
            final BlockSelection before = new BlockSelection();
            final int width = xMax - xMin;
            final int depth = zMax - zMin;
            final int halfWidth = width / 2;
            final int halfDepth = depth / 2;
            before.setPosition(xMin + halfWidth, yMin, zMin + halfDepth);
            before.setSelectionArea(min, max);
            this.pushHistory(Action.REPLACE, new BlockSelectionSnapshot(before));
            final BlockSelection after = new BlockSelection(before);
            final World world = componentAccessor.getExternalData().getWorld();
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, xMin + halfWidth, zMin + halfDepth, Math.max(width, depth));
            final int totalBlocks = (width + 1) * (depth + 1) * (yMax - yMin + 1);
            int counter = 0;
            for (int x = xMin; x <= xMax; ++x) {
                for (int z = zMin; z <= zMax; ++z) {
                    final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    for (int y = yMax; y >= yMin; --y) {
                        final int filler = chunk.getFiller(x, y, z);
                        if (filler != 0) {
                            ++counter;
                            this.sendFeedback("Gather 1/2", totalBlocks, counter, componentAccessor);
                        }
                        else {
                            final int block = chunk.getBlock(x, y, z);
                            if ((doReplace == null && block != 0) || (doReplace != null && doReplace.test(block))) {
                                final Holder<ChunkStore> holder = chunk.getBlockComponentHolder(x, y, z);
                                final Material material = Material.fromPattern(toPattern, this.random);
                                final int newBlockId = material.getBlockId();
                                final int newRotation = material.hasRotation() ? material.getRotation() : chunk.getRotationIndex(x, y, z);
                                final Holder<ChunkStore> newHolder = BuilderToolsPlugin.createBlockComponent(chunk, x, y, z, newBlockId, block, holder, true);
                                final int rotationIndex = chunk.getRotationIndex(x, y, z);
                                before.addBlockAtWorldPos(x, y, z, block, rotationIndex, filler, chunk.getSupportValue(x, y, z), chunk.getBlockComponentHolder(x, y, z));
                                after.addBlockAtWorldPos(x, y, z, newBlockId, newRotation, 0, 0, newHolder);
                                this.replaceMultiBlockStructure(x, y, z, block, newBlockId, newRotation, accessor, before, after);
                                if (newBlockId == 0) {
                                    final int fluidId = chunk.getFluidId(x, y, z);
                                    final byte fluidLevel = chunk.getFluidLevel(x, y, z);
                                    if (fluidId != 0) {
                                        before.addFluidAtWorldPos(x, y, z, fluidId, fluidLevel);
                                        after.addFluidAtWorldPos(x, y, z, 0, (byte)0);
                                    }
                                }
                            }
                            ++counter;
                            this.sendFeedback("Gather 1/2", totalBlocks, counter, componentAccessor);
                        }
                    }
                }
            }
            after.placeNoReturn("Replace 2/2", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute replace of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), after.getBlockCount());
            this.sendUpdate();
            this.sendArea();
            SoundUtil.playSoundEvent2d(ref, TempAssetIdUtil.getSoundEventIndex("CREATE_SELECTION_FILL"), SoundCategory.SFX, componentAccessor);
        }
        
        public void replace(@Nonnull final Ref<EntityStore> ref, @Nonnull final Int2IntFunction function, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final long start = System.nanoTime();
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final int xMin = min.getX();
            final int xMax = max.getX();
            final int yMin = min.getY();
            final int yMax = max.getY();
            final int zMin = min.getZ();
            final int zMax = max.getZ();
            final BlockSelection before = new BlockSelection();
            final int width = xMax - xMin;
            final int depth = zMax - zMin;
            final int halfWidth = width / 2;
            final int halfDepth = depth / 2;
            before.setPosition(xMin + halfWidth, yMin, zMin + halfDepth);
            before.setSelectionArea(min, max);
            this.pushHistory(Action.REPLACE, new BlockSelectionSnapshot(before));
            final BlockSelection after = new BlockSelection(before);
            final World world = componentAccessor.getExternalData().getWorld();
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, xMin + halfWidth, zMin + halfDepth, Math.max(width, depth));
            final int totalBlocks = (width + 1) * (depth + 1) * (yMax - yMin + 1);
            int counter = 0;
            for (int x = xMin; x <= xMax; ++x) {
                for (int z = zMin; z <= zMax; ++z) {
                    final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    for (int y = yMax; y >= yMin; --y) {
                        final int filler = chunk.getFiller(x, y, z);
                        if (filler != 0) {
                            ++counter;
                            this.sendFeedback("Gather 1/2", totalBlocks, counter, componentAccessor);
                        }
                        else {
                            final int block = chunk.getBlock(x, y, z);
                            final int replace = function.applyAsInt(block);
                            if (block != replace) {
                                final Holder<ChunkStore> holder = chunk.getBlockComponentHolder(x, y, z);
                                final Holder<ChunkStore> newHolder = BuilderToolsPlugin.createBlockComponent(chunk, x, y, z, replace, block, holder, true);
                                final int rotationIndex = chunk.getRotationIndex(x, y, z);
                                before.addBlockAtWorldPos(x, y, z, block, rotationIndex, filler, chunk.getSupportValue(x, y, z), holder);
                                after.addBlockAtWorldPos(x, y, z, replace, rotationIndex, 0, 0, newHolder);
                                this.replaceMultiBlockStructure(x, y, z, block, replace, rotationIndex, accessor, before, after);
                                if (replace == 0) {
                                    final int fluidId = chunk.getFluidId(x, y, z);
                                    final byte fluidLevel = chunk.getFluidLevel(x, y, z);
                                    if (fluidId != 0) {
                                        before.addFluidAtWorldPos(x, y, z, fluidId, fluidLevel);
                                        after.addFluidAtWorldPos(x, y, z, 0, (byte)0);
                                    }
                                }
                            }
                            ++counter;
                            this.sendFeedback("Gather 1/2", totalBlocks, counter, componentAccessor);
                        }
                    }
                }
            }
            after.placeNoReturn("Replace 2/2", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute replace of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), after.getBlockCount());
            this.sendUpdate();
            this.sendArea();
        }
        
        public void move(@Nonnull final Ref<EntityStore> ref, @Nonnull final Vector3i direction, final boolean empty, final boolean entities, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final long start = System.nanoTime();
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final int xMin = min.getX();
            final int xMax = max.getX();
            final int yMin = min.getY();
            final int yMax = max.getY();
            final int zMin = min.getZ();
            final int zMax = max.getZ();
            final BlockSelection selected = new BlockSelection();
            final int width = xMax - xMin;
            final int height = yMax - yMin;
            final int depth = zMax - zMin;
            final int halfWidth = width / 2;
            final int halfDepth = depth / 2;
            final int xPos = xMin + halfWidth;
            final int yPos = yMin;
            final int zPos = zMin + halfDepth;
            selected.setPosition(xPos, yPos, zPos);
            final BlockSelection cleared = new BlockSelection(selected);
            final World world = componentAccessor.getExternalData().getWorld();
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, xMin + halfWidth, zMin + halfDepth, Math.max(width, depth) + 16);
            final BlockTypeAssetMap<String, BlockType> blockTypeAssetMap = BlockType.getAssetMap();
            final IndexedLookupTableAssetMap<String, BlockBoundingBoxes> hitboxAssetMap = BlockBoundingBoxes.getAssetMap();
            for (int x = xMin; x <= xMax; ++x) {
                for (int z = zMin; z <= zMax; ++z) {
                    final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    for (int y = yMax; y >= yMin; --y) {
                        final int block = chunk.getBlock(x, y, z);
                        final int fluidId = chunk.getFluidId(x, y, z);
                        final byte fluidLevel = chunk.getFluidLevel(x, y, z);
                        if (block != 0 || fluidId != 0 || empty) {
                            if (this.globalMask == null || !this.globalMask.isExcluded(accessor, x, y, z, min, max, block, fluidId)) {
                                final int filler = chunk.getFiller(x, y, z);
                                final int rotationIndex = chunk.getRotationIndex(x, y, z);
                                selected.addBlockAtWorldPos(x, y, z, block, rotationIndex, filler, chunk.getSupportValue(x, y, z), chunk.getBlockComponentHolder(x, y, z));
                                selected.addFluidAtWorldPos(x, y, z, fluidId, fluidLevel);
                                cleared.addBlockAtWorldPos(x, y, z, 0, 0, 0, 0);
                                cleared.addFluidAtWorldPos(x, y, z, 0, (byte)0);
                                if (filler == 0 && block != 0) {
                                    final BlockType blockType = blockTypeAssetMap.getAsset(block);
                                    if (blockType != null) {
                                        final BlockBoundingBoxes hitbox = hitboxAssetMap.getAsset(blockType.getHitboxTypeIndex());
                                        if (hitbox != null && hitbox.protrudesUnitBox()) {
                                            final int baseX = x;
                                            final int baseY = y;
                                            final int baseZ = z;
                                            FillerBlockUtil.forEachFillerBlock(hitbox.get(rotationIndex), (fx, fy, fz) -> {
                                                if (fx == 0 && fy == 0 && fz == 0) {
                                                    return;
                                                }
                                                else {
                                                    final int fillerX = baseX + fx;
                                                    final int fillerY = baseY + fy;
                                                    final int fillerZ = baseZ + fz;
                                                    if (fillerX >= xMin && fillerX <= xMax && fillerY >= yMin && fillerY <= yMax && fillerZ >= zMin && fillerZ <= zMax) {
                                                        return;
                                                    }
                                                    else {
                                                        final WorldChunk fillerChunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(fillerX, fillerZ));
                                                        final int fillerBlock = fillerChunk.getBlock(fillerX, fillerY, fillerZ);
                                                        final int fillerFiller = fillerChunk.getFiller(fillerX, fillerY, fillerZ);
                                                        if (fillerFiller != 0) {
                                                            final int fillerRotation = fillerChunk.getRotationIndex(fillerX, fillerY, fillerZ);
                                                            selected.addBlockAtWorldPos(fillerX, fillerY, fillerZ, fillerBlock, fillerRotation, fillerFiller, fillerChunk.getSupportValue(fillerX, fillerY, fillerZ), fillerChunk.getBlockComponentHolder(fillerX, fillerY, fillerZ));
                                                            cleared.addBlockAtWorldPos(fillerX, fillerY, fillerZ, 0, 0, 0, 0);
                                                        }
                                                        return;
                                                    }
                                                }
                                            });
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            final BlockSelection beforeCleared = cleared.place(this.player, world);
            selected.setPosition(xPos + direction.getX(), yPos + direction.getY(), zPos + direction.getZ());
            final BlockSelection beforePlace = selected.place(this.player, world);
            final List<SelectionSnapshot<?>> snapshots = new ObjectArrayList<SelectionSnapshot<?>>();
            if (entities) {
                final List<Ref<EntityStore>> targetEntities = TargetUtil.getAllEntitiesInBox(min.toVector3d(), max.toVector3d(), componentAccessor);
                for (final Ref<EntityStore> targetEntityRef : targetEntities) {
                    snapshots.add(new EntityTransformSnapshot(targetEntityRef, componentAccessor));
                    final TransformComponent transformComponent = componentAccessor.getComponent(targetEntityRef, TransformComponent.getComponentType());
                    if (transformComponent != null) {
                        transformComponent.getPosition().add(direction);
                    }
                }
            }
            beforePlace.add(beforeCleared);
            final ClipboardBoundsSnapshot clipboardSnapshot = new ClipboardBoundsSnapshot(min, max);
            final Vector3i destMin = min.clone().add(direction);
            final Vector3i destMax = max.clone().add(direction);
            beforePlace.setSelectionArea(Vector3i.min(min, destMin), Vector3i.max(max, destMax));
            snapshots.add(new BlockSelectionSnapshot(beforePlace));
            snapshots.add(clipboardSnapshot);
            this.pushHistory(Action.MOVE, snapshots);
            BuilderToolsPlugin.invalidateWorldMapForSelection(cleared, world);
            BuilderToolsPlugin.invalidateWorldMapForSelection(selected, world);
            this.selection.setSelectionArea(min.add(direction), max.add(direction));
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute move of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), cleared.getBlockCount());
            this.sendUpdate();
            this.sendArea();
            this.sendFeedback(Message.translation("server.builderTools.selectionMovedBy").param("x", direction.getX()).param("y", direction.getY()).param("z", direction.getZ()), componentAccessor);
        }
        
        public void shift(@Nonnull final Ref<EntityStore> ref, @Nonnull final Vector3i direction, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            this.pushHistory(Action.UPDATE_SELECTION, new ClipboardBoundsSnapshot(this.selection));
            this.selection.setSelectionArea(this.selection.getSelectionMin().add(direction), this.selection.getSelectionMax().add(direction));
            this.sendArea();
            this.sendFeedback(Message.translation("server.builderTools.selectionShiftedBy").param("x", direction.getX()).param("y", direction.getY()).param("z", direction.getZ()), componentAccessor);
        }
        
        public void pos1(@Nonnull final Vector3i pos1, final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection != null && !this.selection.getSelectionMax().equals(Vector3i.ZERO)) {
                this.pushHistory(Action.UPDATE_SELECTION, new ClipboardBoundsSnapshot(this.selection));
                this.selection.setSelectionArea(pos1, this.selection.getSelectionMax());
                this.sendArea();
            }
            else {
                if (this.selection == null) {
                    this.selection = new BlockSelection();
                }
                this.pushHistory(Action.UPDATE_SELECTION, ClipboardBoundsSnapshot.EMPTY);
                this.selection.setSelectionArea(pos1, pos1);
                this.sendArea();
            }
            this.sendFeedback(Message.translation("server.builderTools.setPosTo").param("num", 1).param("x", pos1.getX()).param("y", pos1.getY()).param("z", pos1.getZ()), componentAccessor);
        }
        
        public void pos2(@Nonnull final Vector3i pos2, final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection != null && !this.selection.getSelectionMin().equals(Vector3i.ZERO)) {
                this.pushHistory(Action.UPDATE_SELECTION, new ClipboardBoundsSnapshot(this.selection));
                this.selection.setSelectionArea(this.selection.getSelectionMin(), pos2);
                this.sendArea();
            }
            else {
                if (this.selection == null) {
                    this.selection = new BlockSelection();
                }
                this.pushHistory(Action.UPDATE_SELECTION, ClipboardBoundsSnapshot.EMPTY);
                this.selection.setSelectionArea(pos2, pos2);
                this.sendArea();
            }
            this.sendFeedback(Message.translation("server.builderTools.setPosTo").param("num", 2).param("x", pos2.getX()).param("y", pos2.getY()).param("z", pos2.getZ()), componentAccessor);
        }
        
        public void select(@Nonnull final Vector3i pos1, @Nonnull final Vector3i pos2, @Nullable final String reason, final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection != null && !this.selection.getSelectionMax().equals(Vector3i.ZERO)) {
                this.pushHistory(Action.UPDATE_SELECTION, new ClipboardBoundsSnapshot(this.selection));
                this.selection.setSelectionArea(pos1, pos2);
                this.sendArea();
            }
            else {
                if (this.selection == null) {
                    this.selection = new BlockSelection();
                }
                this.pushHistory(Action.UPDATE_SELECTION, ClipboardBoundsSnapshot.EMPTY);
                this.selection.setSelectionArea(pos1, pos2);
                this.sendArea();
            }
            if (reason != null) {
                final Message reasonMessage = Message.translation(reason);
                this.sendFeedback(Message.translation("server.builderTools.selectedWithReason").param("reason", reasonMessage).param("x1", pos1.getX()).param("y1", pos1.getY()).param("z1", pos1.getZ()).param("x2", pos2.getX()).param("y2", pos2.getY()).param("z2", pos2.getZ()), componentAccessor);
            }
            else {
                this.sendFeedback(Message.translation("server.builderTools.selected").param("x1", pos1.getX()).param("y1", pos1.getY()).param("z1", pos1.getZ()).param("x2", pos2.getX()).param("y2", pos2.getY()).param("z2", pos2.getZ()), componentAccessor);
            }
        }
        
        public void deselect(final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection != null && this.selection.hasSelectionBounds()) {
                this.selection.setSelectionArea(Vector3i.ZERO, Vector3i.ZERO);
                final EditorBlocksChange packet = new EditorBlocksChange();
                packet.selection = null;
                this.player.getPlayerConnection().write(packet);
                this.sendFeedback(Message.translation("server.builderTools.deselected"), componentAccessor);
            }
            else {
                this.sendFeedback(Message.translation("server.builderTools.noSelectionToDeselect"), componentAccessor);
            }
        }
        
        public void stack(@Nonnull final Ref<EntityStore> ref, @Nonnull final Vector3i direction, final int count, final boolean empty, final int spacing, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final long start = System.nanoTime();
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final int xMin = min.getX();
            final int xMax = max.getX();
            final int yMin = min.getY();
            final int yMax = max.getY();
            final int zMin = min.getZ();
            final int zMax = max.getZ();
            final BlockSelection selected = new BlockSelection();
            final int width = xMax - xMin;
            final int depth = zMax - zMin;
            final int halfWidth = width / 2;
            final int halfDepth = depth / 2;
            selected.setPosition(xMin + halfWidth, yMin, zMin + halfDepth);
            final World world = componentAccessor.getExternalData().getWorld();
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, xMin + halfWidth, zMin + halfDepth, Math.max(width, depth));
            for (int x = xMin; x <= xMax; ++x) {
                for (int z = zMin; z <= zMax; ++z) {
                    final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    for (int y = yMax; y >= yMin; --y) {
                        final int block = chunk.getBlock(x, y, z);
                        final int fluidId = chunk.getFluidId(x, y, z);
                        final byte fluidLevel = chunk.getFluidLevel(x, y, z);
                        if (block != 0 || fluidId != 0 || empty) {
                            if (this.globalMask == null || !this.globalMask.isExcluded(accessor, x, y, z, min, max, block, fluidId)) {
                                selected.addBlockAtWorldPos(x, y, z, block, chunk.getRotationIndex(x, y, z), chunk.getFiller(x, y, z), chunk.getSupportValue(x, y, z), chunk.getBlockComponentHolder(x, y, z));
                                selected.addFluidAtWorldPos(x, y, z, fluidId, fluidLevel);
                            }
                        }
                    }
                }
            }
            final BlockSelection before = new BlockSelection();
            before.setAnchor(selected.getAnchorX(), selected.getAnchorY(), selected.getAnchorZ());
            before.setPosition(selected.getX(), selected.getY(), selected.getZ());
            final Vector3i size = max.subtract(min).add(1, 1, 1);
            for (int i = 1; i <= count; ++i) {
                selected.setPosition(before.getX() + (size.getX() + spacing) * direction.getX() * i, before.getY() + (size.getY() + spacing) * direction.getY() * i, before.getZ() + (size.getZ() + spacing) * direction.getZ() * i);
                before.add(selected.place(this.player, world));
            }
            final Vector3i stackOffset = new Vector3i((size.getX() + spacing) * direction.getX() * count, (size.getY() + spacing) * direction.getY() * count, (size.getZ() + spacing) * direction.getZ() * count);
            final Vector3i totalMin = Vector3i.min(min, min.add(stackOffset));
            final Vector3i totalMax = Vector3i.max(max, max.add(stackOffset));
            before.setSelectionArea(totalMin, totalMax);
            this.pushHistory(Action.STACK, new BlockSelectionSnapshot(before));
            BuilderToolsPlugin.invalidateWorldMapForSelection(before, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute stack of %d blocks %d times", diff, TimeUnit.NANOSECONDS.toMillis(diff), selected.getBlockCount(), count);
            this.sendUpdate();
            this.sendArea();
            this.sendFeedback(Message.translation("server.builderTools.selectionStacked").param("count", count).param("x", direction.getX()).param("y", direction.getY()).param("z", direction.getZ()), componentAccessor);
        }
        
        public void expand(@Nonnull final Ref<EntityStore> ref, @Nonnull final Vector3i direction, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            this.pushHistory(Action.UPDATE_SELECTION, new ClipboardBoundsSnapshot(this.selection));
            Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            if (direction.getX() < 0) {
                min = min.add(direction.getX(), 0, 0);
            }
            else if (direction.getX() > 0) {
                max = max.add(direction.getX(), 0, 0);
            }
            if (direction.getY() < 0) {
                min = min.add(0, direction.getY(), 0);
            }
            else if (direction.getY() > 0) {
                max = max.add(0, direction.getY(), 0);
            }
            if (direction.getZ() < 0) {
                min = min.add(0, 0, direction.getZ());
            }
            else if (direction.getZ() > 0) {
                max = max.add(0, 0, direction.getZ());
            }
            this.selection.setSelectionArea(min, max);
            this.sendArea();
            this.sendFeedback(Message.translation("server.builderTools.selectionExpanded").param("x", direction.getX()).param("y", direction.getY()).param("z", direction.getZ()), componentAccessor);
        }
        
        public void contract(@Nonnull final Ref<EntityStore> ref, @Nonnull final Vector3i direction, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            this.pushHistory(Action.UPDATE_SELECTION, new ClipboardBoundsSnapshot(this.selection));
            Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            if (direction.getX() > 0) {
                min = min.add(direction.getX(), 0, 0);
            }
            else if (direction.getX() < 0) {
                max = max.add(direction.getX(), 0, 0);
            }
            if (direction.getY() > 0) {
                min = min.add(0, direction.getY(), 0);
            }
            else if (direction.getY() < 0) {
                max = max.add(0, direction.getY(), 0);
            }
            if (direction.getZ() > 0) {
                min = min.add(0, 0, direction.getZ());
            }
            else if (direction.getZ() < 0) {
                max = max.add(0, 0, direction.getZ());
            }
            this.selection.setSelectionArea(min, max);
            this.sendArea();
            this.sendFeedback(ref, Message.translation("server.builderTools.selectionContracted").param("x", direction.getX()).param("y", direction.getY()).param("z", direction.getZ()), (direction.length() > 0.0) ? "CREATE_SCALE_INCREASE" : "CREATE_SCALE_DECREASE", componentAccessor);
        }
        
        public void repairFillers(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            if (!this.selection.hasSelectionBounds()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final long start = System.nanoTime();
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final int xMin = min.getX();
            final int xMax = max.getX();
            final int yMin = min.getY();
            final int yMax = max.getY();
            final int zMin = min.getZ();
            final int zMax = max.getZ();
            final int totalBlocks = (xMax - xMin + 1) * (zMax - zMin + 1) * (yMax - yMin + 1);
            final int width = xMax - xMin;
            final int height = yMax - yMin;
            final int depth = zMax - zMin;
            final int halfWidth = width / 2;
            final int halfHeight = height / 2;
            final int halfDepth = depth / 2;
            final BlockSelection before = new BlockSelection(totalBlocks, 0);
            before.setPosition(xMin + halfWidth, yMin, zMin + halfDepth);
            before.setSelectionArea(min, max);
            this.pushHistory(Action.SET, new BlockSelectionSnapshot(before));
            final BlockSelection after = new BlockSelection(totalBlocks, 0);
            after.copyPropertiesFrom(before);
            final World world = componentAccessor.getExternalData().getWorld();
            final Store<ChunkStore> chunkStore = world.getChunkStore().getStore();
            final CachedAccessor cachedAccessor = CachedAccessor.of(chunkStore, ChunkUtil.chunkCoordinate(xMin + halfWidth), ChunkUtil.chunkCoordinate(yMin + halfHeight), ChunkUtil.chunkCoordinate(zMin + halfDepth), Math.max(Math.max(width, depth), height));
            final BlockTypeAssetMap<String, BlockType> blockTypeMap = BlockType.getAssetMap();
            final IndexedLookupTableAssetMap<String, BlockBoundingBoxes> blockHitboxMap = BlockBoundingBoxes.getAssetMap();
            int counter = 0;
            for (int x = xMin; x <= xMax; ++x) {
                final int cx = ChunkUtil.chunkCoordinate(x);
                for (int z = zMin; z <= zMax; ++z) {
                    final int cz = ChunkUtil.chunkCoordinate(z);
                    final Ref<ChunkStore> chunkRef = cachedAccessor.getChunk(cx, cz);
                    final WorldChunk wc = chunkStore.getComponent(chunkRef, WorldChunk.getComponentType());
                    for (int y = yMax; y >= yMin; --y) {
                        final int cy = ChunkUtil.chunkCoordinate(y);
                        final BlockSection chunk = cachedAccessor.getBlockSection(cx, cy, cz);
                        if (chunk != null) {
                            final int block = chunk.get(x, y, z);
                            final BlockType blockType = blockTypeMap.getAsset(block);
                            if (blockType != null) {
                                final BlockPhysics physics = cachedAccessor.getBlockPhysics(cx, cy, cz);
                                final BlockBoundingBoxes hitbox = blockHitboxMap.getAsset(blockType.getHitboxTypeIndex());
                                if (chunk.getFiller(x, y, z) != 0) {
                                    before.copyFromAtWorld(x, y, z, wc, physics);
                                    after.copyFromAtWorld(x, y, z, wc, physics);
                                }
                                else if (hitbox != null && hitbox.protrudesUnitBox()) {
                                    before.copyFromAtWorld(x, y, z, wc, physics);
                                    after.copyFromAtWorld(x, y, z, wc, physics);
                                    final int finalX = x;
                                    final int finalY = y;
                                    final int finalZ = z;
                                    FillerBlockUtil.forEachFillerBlock(hitbox.get(chunk.getRotationIndex(x, y, z)), (x1, y1, z1) -> before.copyFromAtWorld(finalX + x1, finalY + y1, finalZ + z1, wc, physics));
                                }
                                ++counter;
                                this.sendFeedback("Gather 1/2", totalBlocks, counter, componentAccessor);
                            }
                        }
                    }
                }
            }
            after.tryFixFiller(false);
            after.placeNoReturn("Set 2/2", this.player, BuilderToolsPlugin.FEEDBACK_CONSUMER, world, componentAccessor);
            BuilderToolsPlugin.invalidateWorldMapForSelection(after, world);
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute repair of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), after.getBlockCount());
            this.sendUpdate();
            this.sendArea();
        }
        
        @Nonnull
        public List<ActionEntry> undo(@Nonnull final Ref<EntityStore> ref, final int count, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            final long start = System.nanoTime();
            final BlockSelection before = this.selection;
            final List<ActionEntry> list = new ObjectArrayList<ActionEntry>();
            for (int i = 0; i < count; ++i) {
                final ActionEntry action = this.historyAction(ref, this.undo, this.redo, componentAccessor);
                if (action == null) {
                    break;
                }
                list.add(action);
            }
            if (before != this.selection) {
                this.sendUpdate();
            }
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute undo of %d actions", diff, TimeUnit.NANOSECONDS.toMillis(diff), count);
            if (list.isEmpty()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.nothingToUndo"), componentAccessor);
            }
            else {
                int j = 0;
                for (final ActionEntry pair : list) {
                    this.sendFeedback(ref, Message.translation("server.builderTools.undoStatus").param("index", ++j).param("action", pair.getAction().name()), "CREATE_UNDO", componentAccessor);
                }
            }
            return list;
        }
        
        @Nonnull
        public List<ActionEntry> redo(@Nonnull final Ref<EntityStore> ref, final int count, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            final long start = System.nanoTime();
            final List<ActionEntry> list = new ObjectArrayList<ActionEntry>();
            for (int i = 0; i < count; ++i) {
                final ActionEntry action = this.historyAction(ref, this.redo, this.undo, componentAccessor);
                if (action == null) {
                    break;
                }
                list.add(action);
            }
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute redo of %d actions", diff, TimeUnit.NANOSECONDS.toMillis(diff), count);
            if (list.isEmpty()) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.nothingToRedo"), componentAccessor);
            }
            else {
                int j = 0;
                for (final ActionEntry pair : list) {
                    this.sendFeedback(ref, Message.translation("server.builderTools.redoStatus").param("index", ++j).param("action", pair.getAction().name()), "CREATE_REDO", componentAccessor);
                }
            }
            return list;
        }
        
        public void save(@Nonnull final Ref<EntityStore> ref, @Nonnull String name, final boolean relativize, final boolean overwrite, final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelection"), componentAccessor);
                return;
            }
            final long start = System.nanoTime();
            if (!name.endsWith(".prefab.json")) {
                name += ".prefab.json";
            }
            final PrefabStore prefabStore = PrefabStore.get();
            final Path serverPrefabsPath = prefabStore.getServerPrefabsPath();
            if (!PathUtil.isChildOf(serverPrefabsPath, serverPrefabsPath.resolve(name)) && !SingleplayerModule.isOwner(this.playerRef)) {
                this.sendFeedback(Message.translation("server.builderTools.attemptedToSaveOutsidePrefabsDir"), componentAccessor);
                return;
            }
            try {
                final BlockSelection postClone = relativize ? this.selection.relativize() : this.selection.cloneSelection();
                prefabStore.saveServerPrefab(name, postClone, overwrite);
                this.sendUpdate();
                this.sendFeedback(Message.translation("server.builderTools.savedSelectionToPrefab").param("name", name), componentAccessor);
            }
            catch (final PrefabSaveException e) {
                switch (e.getType()) {
                    case ALREADY_EXISTS: {
                        BuilderToolsPlugin.get().getLogger().at(Level.WARNING).log("Prefab already exists %s", name);
                        this.sendFeedback(Message.translation("server.builderTools.prefabAlreadyExists"), componentAccessor);
                        break;
                    }
                    case ERROR: {
                        BuilderToolsPlugin.get().getLogger().at(Level.WARNING).withCause(e).log("Exception saving prefab %s", name);
                        this.sendFeedback(Message.translation("server.builderTools.errorSavingPrefab").param("name", name).param("message", e.getCause().getMessage()), componentAccessor);
                        break;
                    }
                }
            }
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute save of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), this.selection.getBlockCount());
        }
        
        public void saveFromSelection(@Nonnull final Ref<EntityStore> ref, @Nonnull final String name, final boolean relativize, final boolean overwrite, final boolean includeEntities, final boolean includeEmpty, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            this.saveFromSelection(ref, name, relativize, overwrite, includeEntities, includeEmpty, null, componentAccessor);
        }
        
        public void saveFromSelection(@Nonnull final Ref<EntityStore> ref, @Nonnull String name, final boolean relativize, final boolean overwrite, final boolean includeEntities, final boolean includeEmpty, @Nullable final Vector3i playerAnchor, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            if (this.selection == null || (this.selection.getSelectionMin().equals(Vector3i.ZERO) && this.selection.getSelectionMax().equals(Vector3i.ZERO))) {
                this.sendErrorFeedback(ref, Message.translation("server.builderTools.noSelectionBounds"), componentAccessor);
                return;
            }
            final World world = componentAccessor.getExternalData().getWorld();
            final long start = System.nanoTime();
            if (!name.endsWith(".prefab.json")) {
                name += ".prefab.json";
            }
            final PrefabStore prefabStore = PrefabStore.get();
            final Path serverPrefabsPath = prefabStore.getServerPrefabsPath();
            if (!PathUtil.isChildOf(serverPrefabsPath, serverPrefabsPath.resolve(name)) && !SingleplayerModule.isOwner(this.playerRef)) {
                this.sendFeedback(Message.translation("server.builderTools.attemptedToSaveOutsidePrefabsDir"), componentAccessor);
                return;
            }
            final Vector3i min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final Vector3i max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
            final int xMin = min.getX();
            final int yMin = min.getY();
            final int zMin = min.getZ();
            final int xMax = max.getX();
            final int yMax = max.getY();
            final int zMax = max.getZ();
            final int width = xMax - xMin;
            final int height = yMax - yMin;
            final int depth = zMax - zMin;
            final int halfWidth = width / 2;
            final int halfDepth = depth / 2;
            final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atWorldCoords(world, xMin + halfWidth, zMin + halfDepth, Math.max(width, depth));
            final BlockTypeAssetMap<String, BlockType> assetMap = BlockType.getAssetMap();
            final int editorBlock = assetMap.getIndex("Editor_Block");
            final int editorBlockPrefabAir = assetMap.getIndex("Editor_Empty");
            final int editorBlockPrefabAnchor = assetMap.getIndex("Editor_Anchor");
            final BlockSelection tempSelection = new BlockSelection();
            tempSelection.setPosition(xMin + halfWidth, yMin, zMin + halfDepth);
            tempSelection.setSelectionArea(min, max);
            int count = 0;
            final int top = Math.max(yMin, yMax);
            final int bottom = Math.min(yMin, yMax);
            for (int x = xMin; x <= xMax; ++x) {
                for (int z = zMin; z <= zMax; ++z) {
                    final WorldChunk chunk = accessor.getChunk(ChunkUtil.indexChunkFromBlock(x, z));
                    final Store<ChunkStore> store = chunk.getReference().getStore();
                    final ChunkColumn chunkColumn = store.getComponent(chunk.getReference(), ChunkColumn.getComponentType());
                    int lastSection = -1;
                    BlockPhysics blockPhysics = null;
                    for (int y = top; y >= bottom; --y) {
                        final int block = chunk.getBlock(x, y, z);
                        final int fluid = chunk.getFluidId(x, y, z);
                        if (lastSection != ChunkUtil.chunkCoordinate(y)) {
                            lastSection = ChunkUtil.chunkCoordinate(y);
                            final Ref<ChunkStore> section = chunkColumn.getSection(lastSection);
                            if (section != null) {
                                blockPhysics = store.getComponent(section, BlockPhysics.getComponentType());
                            }
                            else {
                                blockPhysics = null;
                            }
                        }
                        if (block == editorBlockPrefabAnchor && playerAnchor == null) {
                            tempSelection.setAnchorAtWorldPos(x, y, z);
                            final int id = BuilderToolsPlugin.getNonEmptyNeighbourBlock(accessor, x, y, z);
                            if (id > 0 && id != editorBlockPrefabAir) {
                                tempSelection.addBlockAtWorldPos(x, y, z, id, 0, 0, 0);
                                ++count;
                            }
                            else if (id == editorBlockPrefabAir) {
                                tempSelection.addBlockAtWorldPos(x, y, z, 0, 0, 0, 0);
                                ++count;
                            }
                        }
                        else if ((block != 0 || fluid != 0 || includeEmpty) && block != editorBlock) {
                            if (block == editorBlockPrefabAir) {
                                tempSelection.addBlockAtWorldPos(x, y, z, 0, 0, 0, 0);
                            }
                            else {
                                tempSelection.copyFromAtWorld(x, y, z, chunk, blockPhysics);
                            }
                            ++count;
                        }
                    }
                }
            }
            if (playerAnchor != null) {
                tempSelection.setAnchorAtWorldPos(playerAnchor.getX(), playerAnchor.getY(), playerAnchor.getZ());
            }
            final PrefabSaveException e;
            if (includeEntities) {
                final Store<EntityStore> entityStore = world.getEntityStore().getStore();
                BuilderToolsPlugin.forEachCopyableInSelection(world, xMin, yMin, zMin, width, height, depth, e -> {
                    final Holder<EntityStore> holder = entityStore.copyEntity(e);
                    tempSelection.addEntityFromWorld(holder);
                    return;
                });
            }
            try {
                final BlockSelection postClone = relativize ? tempSelection.relativize() : tempSelection.cloneSelection();
                prefabStore.saveServerPrefab(name, postClone, overwrite);
                this.sendFeedback(Message.translation("server.builderTools.savedSelectionToPrefab").param("name", name), componentAccessor);
            }
            catch (final PrefabSaveException e) {
                switch (e.getType()) {
                    case ALREADY_EXISTS: {
                        BuilderToolsPlugin.get().getLogger().at(Level.WARNING).log("Prefab already exists %s", name);
                        this.sendFeedback(Message.translation("server.builderTools.prefabAlreadyExists"), componentAccessor);
                        break;
                    }
                    case ERROR: {
                        BuilderToolsPlugin.get().getLogger().at(Level.WARNING).withCause(e).log("Exception saving prefab %s", name);
                        this.sendFeedback(Message.translation("server.builderTools.errorSavingPrefab").param("name", name).param("message", e.getCause().getMessage()), componentAccessor);
                        break;
                    }
                }
            }
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute saveFromSelection of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), count);
        }
        
        public void load(@Nonnull String name, final ComponentAccessor<EntityStore> componentAccessor) {
            if (!name.endsWith(".prefab.json")) {
                name += ".prefab.json";
            }
            this.load(name, PrefabStore.get().getServerPrefab(name), componentAccessor);
        }
        
        public void load(@Nonnull final String name, @Nonnull final BlockSelection serverPrefab, final ComponentAccessor<EntityStore> componentAccessor) {
            final long start = System.nanoTime();
            try {
                Vector3i min = Vector3i.ZERO;
                Vector3i max = Vector3i.ZERO;
                if (this.selection != null) {
                    Objects.requireNonNull(this.selection.getSelectionMin(), "min is null");
                    Objects.requireNonNull(this.selection.getSelectionMax(), "max is null");
                    min = Vector3i.min(this.selection.getSelectionMin(), this.selection.getSelectionMax());
                    max = Vector3i.max(this.selection.getSelectionMin(), this.selection.getSelectionMax());
                }
                (this.selection = serverPrefab.cloneSelection()).setSelectionArea(min, max);
                this.sendUpdate();
                this.sendFeedback(Message.translation("server.general.loadedPrefab").param("name", name), componentAccessor);
            }
            catch (final PrefabLoadException e) {
                switch (e.getType()) {
                    case NOT_FOUND: {
                        BuilderToolsPlugin.get().getLogger().at(Level.WARNING).log("Prefab doesn't exist %s", name);
                        this.sendFeedback(Message.translation("server.builderTools.prefabDoesNotExist").param("name", name), componentAccessor);
                        break;
                    }
                    case ERROR: {
                        BuilderToolsPlugin.get().getLogger().at(Level.WARNING).withCause(e).log("Exception loading prefab %s", name);
                        this.sendFeedback(Message.translation("server.builderTools.errorSavingPrefab").param("name", name).param("message", e.getCause().getMessage()), componentAccessor);
                        break;
                    }
                }
            }
            final long end = System.nanoTime();
            final long diff = end - start;
            BuilderToolsPlugin.get().getLogger().at(Level.FINE).log("Took: %dns (%dms) to execute load of %d blocks", diff, TimeUnit.NANOSECONDS.toMillis(diff), this.selection.getBlockCount());
        }
        
        public void clearHistory(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            final long stamp = this.undoLock.writeLock();
            try {
                this.undo.clear();
                this.redo.clear();
            }
            finally {
                this.undoLock.unlockWrite(stamp);
            }
            this.sendFeedback(Message.translation("server.builderTools.historyCleared"), componentAccessor);
        }
        
        public void setGlobalMask(@Nullable final BlockMask mask, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
            this.globalMask = mask;
            if (this.globalMask == null) {
                this.sendFeedback(Message.translation("server.builderTools.maskDisabled"), componentAccessor);
            }
            else {
                this.sendFeedback(Message.translation("server.builderTools.maskSet"), componentAccessor);
            }
        }
        
        private void sendUpdate() {
            this.player.getPlayerConnection().write(Objects.requireNonNullElseGet(this.selection, BlockSelection::new).toPacket());
        }
        
        public void sendArea() {
            if (this.selection != null) {
                this.player.getPlayerConnection().write(this.selection.toSelectionPacket());
            }
            else {
                final EditorBlocksChange packet = new EditorBlocksChange();
                packet.selection = null;
                this.player.getPlayerConnection().write(packet);
            }
        }
        
        private void pushHistory(final Action action, final SelectionSnapshot<?> snapshot) {
            this.pushHistory(action, Collections.singletonList(snapshot));
        }
        
        private void pushHistory(final Action action, final List<SelectionSnapshot<?>> snapshots) {
            if (action == Action.UPDATE_SELECTION && !this.getUserData().isRecordingSelectionHistory()) {
                return;
            }
            final long stamp = this.undoLock.writeLock();
            try {
                this.undo.enqueue(new ActionEntry(action, snapshots));
                this.redo.clear();
                while (this.undo.size() > BuilderToolsPlugin.get().historyCount) {
                    this.undo.dequeue();
                }
            }
            finally {
                this.undoLock.unlockWrite(stamp);
            }
            if (action != Action.UPDATE_SELECTION && action != Action.COPY && action != Action.CUT_COPY) {
                this.markPrefabsDirtyFromSnapshots(snapshots);
            }
        }
        
        private void markPrefabsDirtyFromSnapshots(@Nonnull final List<SelectionSnapshot<?>> snapshots) {
            final PrefabEditSessionManager prefabEditSessionManager = BuilderToolsPlugin.get().getPrefabEditSessionManager();
            final Map<UUID, PrefabEditSession> activeEditSessions = prefabEditSessionManager.getActiveEditSessions();
            if (activeEditSessions.isEmpty()) {
                return;
            }
            for (final SelectionSnapshot<?> snapshot : snapshots) {
                if (snapshot instanceof final BlockSelectionSnapshot blockSnapshot) {
                    final BlockSelection blockSelection = blockSnapshot.getBlockSelection();
                    final Vector3i min = blockSelection.getSelectionMin();
                    final Vector3i max = blockSelection.getSelectionMax();
                    for (final Map.Entry<UUID, PrefabEditSession> entry : activeEditSessions.entrySet()) {
                        entry.getValue().markPrefabsDirtyInBounds(min, max);
                    }
                }
            }
        }
        
        @Nullable
        private ActionEntry historyAction(final Ref<EntityStore> ref, @Nonnull final ObjectArrayFIFOQueue<ActionEntry> from, @Nonnull final ObjectArrayFIFOQueue<ActionEntry> to, final ComponentAccessor<EntityStore> componentAccessor) {
            final long stamp = this.undoLock.writeLock();
            try {
                if (from.isEmpty()) {
                    return null;
                }
                final ActionEntry builderAction = from.dequeueLast();
                to.enqueue(builderAction.restore(ref, this.player, componentAccessor.getExternalData().getWorld(), componentAccessor));
                while (to.size() > BuilderToolsPlugin.get().historyCount) {
                    to.dequeue();
                }
                return builderAction;
            }
            finally {
                this.undoLock.unlockWrite(stamp);
            }
        }
        
        static {
            STATE_METRICS_REGISTRY = new MetricsRegistry<BuilderState>().register("Uuid", state -> state.player.getUuid(), (Codec<UUID>)Codec.UUID_STRING).register("Username", BuilderState::getDisplayName, Codec.STRING).register("ActivePrefabPath", BuilderState::getActivePrefabPath, Codec.UUID_STRING).register("Selection", BuilderState::getSelection, BlockSelection.METRICS_REGISTRY).register("TaskFuture", state -> Objects.toString(state.getTaskFuture()), (Codec<String>)Codec.STRING).register("TaskCount", BuilderState::getTaskCount, Codec.INTEGER).register("UndoCount", BuilderState::getUndoCount, Codec.INTEGER).register("RedoCount", BuilderState::getRedoCount, Codec.INTEGER);
        }
        
        public static class BlocksSampleData
        {
            public int mainBlock;
            public int mainBlockCount;
            public int mainBlockNotAir;
            public int mainBlockNotAirCount;
            
            public BlocksSampleData() {
                this.mainBlock = 0;
                this.mainBlockCount = 0;
                this.mainBlockNotAir = 0;
                this.mainBlockNotAirCount = 0;
            }
        }
        
        public static class SmoothSampleData
        {
            public float solidStrength;
            public int solidBlock;
            public int solidBlockCount;
            public int fillerBlock;
            public int fillerBlockCount;
            
            public SmoothSampleData() {
                this.solidStrength = 0.0f;
                this.solidBlock = 0;
                this.solidBlockCount = 0;
                this.fillerBlock = 0;
                this.fillerBlockCount = 0;
            }
        }
    }
    
    public static class BuilderToolsConfig
    {
        public static final BuilderCodec<BuilderToolsConfig> CODEC;
        private int historyCount;
        private long toolExpireTime;
        
        public BuilderToolsConfig() {
            this.historyCount = 50;
            this.toolExpireTime = 3600L;
        }
        
        static {
            CODEC = BuilderCodec.builder(BuilderToolsConfig.class, BuilderToolsConfig::new).append(new KeyedCodec<Integer>("HistoryCount", Codec.INTEGER), (o, i) -> o.historyCount = i, o -> o.historyCount).documentation("The number of builder tool edit operations to keep in the undo/redo history").add().append(new KeyedCodec("ToolExpireTime", Codec.LONG), (o, l) -> o.toolExpireTime = l, o -> o.toolExpireTime).documentation("The minimum time (in seconds) that a user's builder tool data will be persisted for after they disconnect from the server. If set to zero the player's data is removed immediately on disconnect").addValidator(Validators.greaterThanOrEqual(0L)).add().build();
        }
    }
    
    public static class CachedAccessor extends AbstractCachedAccessor
    {
        private static final ThreadLocal<CachedAccessor> THREAD_LOCAL;
        private static final int FLUID_COMPONENT = 0;
        private static final int PHYSICS_COMPONENT = 1;
        private static final int BLOCKS_COMPONENT = 2;
        
        public CachedAccessor() {
            super(3);
        }
        
        @Nonnull
        public static CachedAccessor of(final ComponentAccessor<ChunkStore> accessor, final int cx, final int cy, final int cz, final int radius) {
            final CachedAccessor cachedAccessor = CachedAccessor.THREAD_LOCAL.get();
            cachedAccessor.init(accessor, cx, cy, cz, radius);
            return cachedAccessor;
        }
        
        @Nullable
        public FluidSection getFluidSection(final int cx, final int cy, final int cz) {
            return this.getComponentSection(cx, cy, cz, 0, FluidSection.getComponentType());
        }
        
        @Nullable
        public BlockPhysics getBlockPhysics(final int cx, final int cy, final int cz) {
            return this.getComponentSection(cx, cy, cz, 1, BlockPhysics.getComponentType());
        }
        
        @Nullable
        public BlockSection getBlockSection(final int cx, final int cy, final int cz) {
            return this.getComponentSection(cx, cy, cz, 2, BlockSection.getComponentType());
        }
        
        static {
            THREAD_LOCAL = ThreadLocal.withInitial((Supplier<? extends CachedAccessor>)CachedAccessor::new);
        }
    }
}
