/* * WorldEdit, a Minecraft world manipulation toolkit * Copyright (C) sk89q * Copyright (C) WorldEdit team and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.sk89q.worldedit; import com.fastasyncworldedit.core.Fawe; import com.fastasyncworldedit.core.FaweCache; import com.fastasyncworldedit.core.configuration.Caption; import com.fastasyncworldedit.core.configuration.Settings; import com.fastasyncworldedit.core.extent.DisallowedBlocksExtent; import com.fastasyncworldedit.core.extent.FaweRegionExtent; import com.fastasyncworldedit.core.extent.HistoryExtent; import com.fastasyncworldedit.core.extent.LimitExtent; import com.fastasyncworldedit.core.extent.MultiRegionExtent; import com.fastasyncworldedit.core.extent.NullExtent; import com.fastasyncworldedit.core.extent.SingleRegionExtent; import com.fastasyncworldedit.core.extent.SlowExtent; import com.fastasyncworldedit.core.extent.StripNBTExtent; import com.fastasyncworldedit.core.extent.processor.heightmap.HeightmapProcessor; import com.fastasyncworldedit.core.extent.processor.lighting.NullRelighter; import com.fastasyncworldedit.core.extent.processor.lighting.RelightMode; import com.fastasyncworldedit.core.extent.processor.lighting.RelightProcessor; import com.fastasyncworldedit.core.extent.processor.lighting.Relighter; import com.fastasyncworldedit.core.history.DiskStorageHistory; import com.fastasyncworldedit.core.history.MemoryOptimizedHistory; import com.fastasyncworldedit.core.history.RollbackOptimizedHistory; import com.fastasyncworldedit.core.history.changeset.AbstractChangeSet; import com.fastasyncworldedit.core.history.changeset.BlockBagChangeSet; import com.fastasyncworldedit.core.history.changeset.NullChangeSet; import com.fastasyncworldedit.core.limit.FaweLimit; import com.fastasyncworldedit.core.limit.PropertyRemap; import com.fastasyncworldedit.core.queue.IBatchProcessor; import com.fastasyncworldedit.core.queue.IQueueChunk; import com.fastasyncworldedit.core.queue.IQueueExtent; import com.fastasyncworldedit.core.queue.implementation.ParallelQueueExtent; import com.fastasyncworldedit.core.regions.RegionWrapper; import com.fastasyncworldedit.core.util.MemUtil; import com.fastasyncworldedit.core.util.Permission; import com.fastasyncworldedit.core.wrappers.WorldWrapper; import com.google.common.base.Preconditions; import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.event.extent.EditSessionEvent; import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.extension.platform.Locatable; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.extent.inventory.BlockBag; import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.util.Identifiable; import com.sk89q.worldedit.util.eventbus.EventBus; import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.world.World; import org.apache.logging.log4j.Logger; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.HashSet; import java.util.Locale; import java.util.Set; import java.util.UUID; /** * A builder-style factory for {@link EditSession EditSessions}. */ public final class EditSessionBuilder { private static final Logger LOGGER = LogManagerCompat.getLogger(); private final EventBus eventBus; private FaweLimit limit; private AbstractChangeSet changeSet; private Region[] allowedRegions; private Region[] disallowedRegions; private Boolean fastMode; private Boolean checkMemory; private Boolean combineStages; private EditSessionEvent event; private String command; private RelightMode relightMode; private Relighter relighter; private Boolean wnaMode; private AbstractChangeSet changeTask; private Extent bypassHistory; private Extent bypassAll; private Extent extent; private boolean compiled; private boolean wrapped; private @Nullable World world; private int maxBlocks = -1; @Nullable private Actor actor; @Nullable private BlockBag blockBag; private boolean tracing; EditSessionBuilder(EventBus eventBus) { this.eventBus = eventBus; } /** * Set the world for the {@link EditSession}. * * @param world the world * @return this builder */ public EditSessionBuilder world(@Nullable World world) { this.world = world; return setDirty(); } /** * Get the world to be edited if present or null */ @Nullable public World getWorld() { return world; } /** * Get the maximum number of block changes allowed */ public int getMaxBlocks() { return maxBlocks; } /** * Set the maximum blocks to change for the {@link EditSession}. * * @param maxBlocks the maximum blocks to change * @return this builder */ public EditSessionBuilder maxBlocks(int maxBlocks) { this.maxBlocks = maxBlocks; return setDirty(); } /** * Get the actor associated with the edit if present or null */ @Nullable public Actor getActor() { return actor; } /** * Set the actor who owns the {@link EditSession}. * * @param actor the actor * @return this builder */ public EditSessionBuilder actor(@Nullable Actor actor) { this.actor = actor; return setDirty(); } /** * Get the {@link BlockBag} associated with the edit if present or null */ @Nullable public BlockBag getBlockBag() { return blockBag; } /** * Set the block bag for the {@link EditSession}. * * @param blockBag the block bag * @return this builder */ public EditSessionBuilder blockBag(@Nullable BlockBag blockBag) { this.blockBag = blockBag; return setDirty(); } /** * Check if tracing is enabled. * * Internal use only. */ public boolean isTracing() { return tracing; } /** * Set tracing enabled/disabled. * * Internal use only. */ public EditSessionBuilder tracing(boolean tracing) { this.tracing = tracing; return setDirty(); } /** * Set the actor to one with a location/extent associated. Sets both the actor and the world. */ public EditSessionBuilder locatableActor(A locatable) { Extent extent = locatable.getExtent(); Preconditions.checkArgument(extent instanceof World, "%s is not located in a World", locatable); return world(((World) extent)).actor(locatable); } /** * Build the {@link EditSession} using properties described in this builder. * * @return the new EditSession */ public EditSession build() { // TracedEditSession does nothing at the moment. //if (WorldEdit.getInstance().getConfiguration().traceUnflushedSessions) { // return new TracedEditSession(this); //} return new EditSession(this); } /** * Set builder as changed and requiring (re-)compilation */ private EditSessionBuilder setDirty() { compiled = false; return this; } /** * Set the {@link EditSessionEvent} instance to be used for firing at different stages of preparation */ public EditSessionBuilder event(@Nullable EditSessionEvent event) { this.event = event; return setDirty(); } /** * Set the limit(s) for the edit to use */ public EditSessionBuilder limit(@Nullable FaweLimit limit) { this.limit = limit; return setDirty(); } /** * Set the edit to be able to edit everywhere, and for any number of blocks */ public EditSessionBuilder limitUnlimited() { return limit(FaweLimit.MAX.copy()); } /** * Unlimited in regions/block changes, but uses the given {@link Actor}'s inventory mode. */ public EditSessionBuilder limitUnprocessed(@Nonnull Actor player) { limitUnlimited(); FaweLimit tmp = player.getLimit(); limit.INVENTORY_MODE = tmp.INVENTORY_MODE; return setDirty(); } /** * Set the changeset to be used for history */ public EditSessionBuilder changeSet(@Nullable AbstractChangeSet changeSet) { this.changeSet = changeSet; return setDirty(); } /** * Do not process any history */ public EditSessionBuilder changeSetNull() { return changeSet(new NullChangeSet(world)); } /** * Set the command used that created this edit. Used in {@link RollbackOptimizedHistory} */ public EditSessionBuilder command(String command) { this.command = command; return this; } /** * Create a new changeset to be used for the edit's history. * * @param disk If disk should be used for history storage * @param uuid UUID to be used for the history or null if unneeded. */ public EditSessionBuilder changeSet(boolean disk, @Nullable UUID uuid) { if (disk) { if (Settings.settings().HISTORY.USE_DATABASE) { this.changeSet = new RollbackOptimizedHistory(world, uuid); } else { this.changeSet = new DiskStorageHistory(world, uuid); } } else { this.changeSet = new MemoryOptimizedHistory(world); } return setDirty(); } /** * Set the regions the edit is allowed to operate in. Set to null for the regions to be calculated based on the actor if * present */ public EditSessionBuilder allowedRegions(@Nullable Region[] allowedRegions) { this.allowedRegions = allowedRegions; return setDirty(); } /** * Set the regions the edit is allowed to operate in. Set to null for the regions to be calculated based on the actor if * present */ @Deprecated public EditSessionBuilder allowedRegions(@Nullable RegionWrapper[] allowedRegions) { this.allowedRegions = allowedRegions; return setDirty(); } /** * Set the region the edit is allowed to operate in. Set to null for the regions to be calculated based on the actor if * present */ public EditSessionBuilder allowedRegions(@Nullable RegionWrapper allowedRegion) { this.allowedRegions = allowedRegion == null ? null : allowedRegion.toArray(); return setDirty(); } /** * Set the regions the edit is allowed to operate in. Set to null for the regions to be calculated based on the actor if * present */ public EditSessionBuilder disallowedRegions(@Nullable Region[] disallowedRegions) { this.disallowedRegions = disallowedRegions; return setDirty(); } /** * Set the regions the edit is allowed to operate in. Set to null for the regions to be calculated based on the actor if * present */ @Deprecated public EditSessionBuilder disallowedRegions(@Nullable RegionWrapper[] disallowedRegions) { this.disallowedRegions = disallowedRegions; return setDirty(); } /** * Set the region the edit is allowed to operate in. Set to null for the regions to be calculated based on the actor if * present */ public EditSessionBuilder disallowedRegions(@Nullable RegionWrapper disallowedRegion) { this.disallowedRegions = disallowedRegion == null ? null : disallowedRegion.toArray(); return setDirty(); } /** * Set the edit to be allowed to edit everywhere */ public EditSessionBuilder allowedRegionsEverywhere() { return allowedRegions(new Region[]{RegionWrapper.GLOBAL()}); } /** * Set fast mode. Use null to set to actor's fast mode setting. Also set to true by default if history for console disabled */ public EditSessionBuilder fastMode(@Nullable Boolean fastMode) { this.fastMode = fastMode; return setDirty(); } /** * Set the {@link RelightMode} */ public EditSessionBuilder relightMode(@Nullable RelightMode relightMode) { this.relightMode = relightMode; return setDirty(); } /** * Override if memory usage should be checked during editsession compilation. By default, checks memory if fastmode is not * enabled and actor is not null. */ public EditSessionBuilder checkMemory(@Nullable Boolean checkMemory) { this.checkMemory = checkMemory; return setDirty(); } /** * Record history with dispatching:, * - Much faster as it avoids duplicate block checks, * - Slightly worse compression since dispatch order is different. */ public EditSessionBuilder combineStages(@Nullable Boolean combineStages) { this.combineStages = combineStages; return setDirty(); } /** * Compile the builder to the settings given. Prepares history, limits, lighting, etc. */ public EditSessionBuilder compile() { if (compiled) { return this; } compiled = true; wrapped = false; if (event == null) { event = new EditSessionEvent(world, actor, -1, null); } if (actor == null && event.getActor() != null) { actor = event.getActor(); } if (limit == null) { if (actor == null) { limit = FaweLimit.MAX; } else { limit = actor.getLimit(); } } if (fastMode == null) { if (actor == null) { fastMode = !Settings.settings().HISTORY.ENABLE_FOR_CONSOLE; } else { fastMode = actor.getSession().hasFastMode(); } } if (checkMemory == null) { checkMemory = actor != null && !this.fastMode; } if (checkMemory) { if (MemUtil.isMemoryLimitedSlow()) { if (Permission.hasPermission(actor, "worldedit.fast")) { actor.print(Caption.of("fawe.info.worldedit.oom.admin")); } throw FaweCache.LOW_MEMORY; } } // this.originalLimit = limit; this.blockBag = limit.INVENTORY_MODE != 0 ? blockBag : null; this.limit = limit.copy(); if (extent == null) { IQueueExtent queue = null; World unwrapped = WorldWrapper.unwrap(world); boolean placeChunks = (this.fastMode || this.limit.FAST_PLACEMENT) && (wnaMode == null || !wnaMode); if (placeChunks) { wnaMode = false; if (unwrapped instanceof IQueueExtent) { extent = queue = (IQueueExtent) unwrapped; } else if (Settings.settings().QUEUE.PARALLEL_THREADS > 1 && !Fawe.isMainThread()) { ParallelQueueExtent parallel = new ParallelQueueExtent(Fawe.instance().getQueueHandler(), world, fastMode); queue = parallel.getExtent(); extent = parallel; } else { extent = queue = Fawe.instance().getQueueHandler().getQueue(world); } } else { wnaMode = true; extent = world; } if (combineStages == null) { combineStages = // If it's enabled in the settings Settings.settings().HISTORY.COMBINE_STAGES // If fast placement is disabled, it's slower to perform a copy on each chunk && this.limit.FAST_PLACEMENT // If the edit uses items from the inventory we can't use a delayed task && this.blockBag == null; } extent = this.bypassAll = wrapExtent(extent, eventBus, event, EditSession.Stage.BEFORE_CHANGE); this.bypassHistory = this.extent = wrapExtent(bypassAll, eventBus, event, EditSession.Stage.BEFORE_REORDER); if (!this.fastMode || changeSet != null) { if (changeSet == null) { if (Settings.settings().HISTORY.USE_DISK) { UUID uuid = actor == null ? Identifiable.CONSOLE : actor.getUniqueId(); if (Settings.settings().HISTORY.USE_DATABASE) { changeSet = new RollbackOptimizedHistory(world, uuid); } else { changeSet = new DiskStorageHistory(world, uuid); } // } else if (combineStages && Settings.settings().HISTORY.COMPRESSION_LEVEL == 0) { // changeSet = new CPUOptimizedChangeSet(world); } else { if (combineStages && Settings.settings().HISTORY.COMPRESSION_LEVEL == 0) { //TODO add CPUOptimizedChangeSet } changeSet = new MemoryOptimizedHistory(world); } } if (this.limit.SPEED_REDUCTION > 0) { this.extent = this.bypassHistory = new SlowExtent(this.bypassHistory, this.limit.SPEED_REDUCTION); } if (command != null && changeSet instanceof RollbackOptimizedHistory) { ((RollbackOptimizedHistory) changeSet).setCommand(this.command); } if (!(changeSet instanceof NullChangeSet)) { if (this.blockBag != null) { //TODO implement block bag as IBatchProcessor changeSet = new BlockBagChangeSet(changeSet, blockBag, limit.INVENTORY_MODE == 1); } if (combineStages) { changeTask = changeSet; this.extent = extent.enableHistory(changeSet); } else { this.extent = new HistoryExtent(extent, changeSet); // if (this.blockBag != null) { // this.extent = new BlockBagExtent(this.extent, blockBag, limit.INVENTORY_MODE == 1); // } } } } if (allowedRegions == null && Settings.settings().REGION_RESTRICTIONS) { if (actor != null && !actor.hasPermission("fawe.bypass.regions")) { if (actor instanceof Player player) { allowedRegions = player.getAllowedRegions(); } } } if (disallowedRegions == null && Settings.settings().REGION_RESTRICTIONS && Settings.settings().REGION_RESTRICTIONS_OPTIONS.ALLOW_BLACKLISTS) { if (actor != null && !actor.hasPermission("fawe.bypass.regions")) { if (actor instanceof Player player) { disallowedRegions = player.getDisallowedRegions(); } } } FaweRegionExtent regionExtent = null; if (disallowedRegions != null) { // Always use MultiRegionExtent if we have blacklist regions regionExtent = new MultiRegionExtent(this.extent, this.limit, allowedRegions, disallowedRegions); } else if (allowedRegions == null) { allowedRegions = new Region[]{RegionWrapper.GLOBAL()}; } else { if (allowedRegions.length == 0) { regionExtent = new NullExtent(this.extent, FaweCache.NO_REGION); } else { if (allowedRegions.length == 1) { regionExtent = new SingleRegionExtent(this.extent, this.limit, allowedRegions[0]); } else { regionExtent = new MultiRegionExtent(this.extent, this.limit, allowedRegions, null); } } } if (placeChunks && regionExtent != null) { queue.addProcessor(regionExtent); } // There's no need to do the below (and it'll also just be a pain to implement) if we're not placing chunks if (placeChunks) { if (((relightMode != null && relightMode != RelightMode.NONE) || (relightMode == null && Settings.settings().LIGHTING.MODE > 0))) { relighter = WorldEdit.getInstance().getPlatformManager() .queryCapability(Capability.WORLD_EDITING) .getRelighterFactory().createRelighter(relightMode, world, queue); queue.addProcessor(new RelightProcessor(relighter)); } queue.addProcessor(new HeightmapProcessor(world.getMinY(), world.getMaxY())); IBatchProcessor platformProcessor = WorldEdit .getInstance() .getPlatformManager() .queryCapability(Capability.WORLD_EDITING) .getPlatformProcessor(fastMode); if (platformProcessor != null) { queue.addProcessor(platformProcessor); } IBatchProcessor platformPostProcessor = WorldEdit .getInstance() .getPlatformManager() .queryCapability(Capability.WORLD_EDITING) .getPlatformPostProcessor(fastMode); if (platformPostProcessor != null) { queue.addPostProcessor(platformPostProcessor); } } else { relighter = NullRelighter.INSTANCE; } if (limit != null && !limit.isUnlimited() && regionExtent != null) { this.extent = new LimitExtent(regionExtent, limit); } else if (limit != null && !limit.isUnlimited()) { this.extent = new LimitExtent(this.extent, limit); } else if (regionExtent != null) { this.extent = regionExtent; } if (this.limit != null && this.limit.STRIP_NBT != null && !this.limit.STRIP_NBT.isEmpty()) { this.extent = new StripNBTExtent(this.extent, this.limit.STRIP_NBT); if (placeChunks) { queue.addProcessor((IBatchProcessor) this.extent); } } if (this.limit != null && !this.limit.isUnlimited()) { Set limitBlocks = new HashSet<>(); if ((getActor() == null || getActor().hasPermission("worldedit.anyblock")) && this.limit.UNIVERSAL_DISALLOWED_BLOCKS) { limitBlocks.addAll(WorldEdit.getInstance().getConfiguration().disallowedBlocks); } if (this.limit.DISALLOWED_BLOCKS != null && !this.limit.DISALLOWED_BLOCKS.isEmpty()) { limitBlocks.addAll(this.limit.DISALLOWED_BLOCKS); } Set> remaps = this.limit.REMAP_PROPERTIES; if (!limitBlocks.isEmpty() || (remaps != null && !remaps.isEmpty())) { this.extent = new DisallowedBlocksExtent(this.extent, limitBlocks, remaps); if (placeChunks) { queue.addProcessor((IBatchProcessor) this.extent); } } } this.extent = wrapExtent(this.extent, eventBus, event, EditSession.Stage.BEFORE_HISTORY); } return this; } /** * Get the relight engine to be used */ public Relighter getRelighter() { return relighter; } /** * If the edit will force using WNA */ public boolean isWNAMode() { return wnaMode; } /** * get the allowed regions associated with the edit's restricttions */ @Nullable public Region[] getAllowedRegions() { return allowedRegions; } /** * Force WNA to be used instead of FAWE's queue system. Will use more memory, be slower, and more likely to cause issues. */ public EditSessionBuilder forceWNA() { this.wnaMode = true; return setDirty(); } /** * If an {@link EditSessionEvent} has been fired yet */ public boolean isWrapped() { return wrapped; } /** * Get the base extent that blocks are set to, bypassing any restrictions, limits and history. All extents up to and including * {@link com.sk89q.worldedit.EditSession.Stage#BEFORE_REORDER} */ public Extent getBypassHistory() { return bypassHistory; } /** * Get the base extent that blocks are set to, bypassing any restrictions, limits and history. All extents up to and including * {@link com.sk89q.worldedit.EditSession.Stage#BEFORE_CHANGE} */ public Extent getBypassAll() { return bypassAll; } /** * Get the edit's limits */ @Nonnull public FaweLimit getLimit() { return limit; } /** * Get the change set that will be used for history */ public AbstractChangeSet getChangeTask() { return changeTask; } /** * Get the ultimate resultant extent */ public Extent getExtent() { return extent != null ? extent : world; } /** * Fire an {@link EditSessionEvent}. Fired after each stage of preparation, allows other plugins to add/alter extents. */ private Extent wrapExtent( final Extent extent, final EventBus eventBus, EditSessionEvent event, final EditSession.Stage stage ) { event = event.clone(stage); event.setExtent(extent); eventBus.post(event); if (event.isCancelled()) { return new NullExtent(extent, FaweCache.MANUAL); } final Extent toReturn = event.getExtent(); if (toReturn instanceof com.sk89q.worldedit.extent.NullExtent) { return new NullExtent(toReturn, FaweCache.MANUAL); } if (toReturn != extent) { String className = toReturn.getClass().getName().toLowerCase(Locale.ROOT); for (String allowed : Settings.settings().EXTENT.ALLOWED_PLUGINS) { if (className.contains(allowed.toLowerCase(Locale.ROOT))) { this.wrapped = true; return toReturn; } } if (Settings.settings().EXTENT.DEBUG) { if (event.getActor() != null) { event.getActor().printDebug(TextComponent.of("Potentially unsafe extent blocked: " + toReturn .getClass() .getName())); event.getActor().print(TextComponent.of( "- For area restrictions and block logging, it is recommended that third party plugins use the FAWE" + " API")); event.getActor().print(TextComponent.of("- Add the following line to the `allowed-plugins` list in the " + "FAWE config.yml to let FAWE recognize the extent:")); event.getActor().print(toReturn.getClass().getName()); } else { LOGGER.warn("Potentially unsafe extent blocked: {}", toReturn.getClass().getName()); LOGGER.warn( " - For area restrictions and block logging, it is recommended that third party plugins use the FAWE API"); LOGGER.warn( " - Add the following classpath to the `allowed-plugins` list in the FAWE config.yml to let FAWE " + "recognize the extent:"); LOGGER.warn(toReturn.getClass().getName()); } } } return extent; } }