From 100288ada5c898d6686d36d404b27c30722decea Mon Sep 17 00:00:00 2001 From: Jordan Date: Wed, 22 Jun 2022 12:50:22 +0100 Subject: [PATCH] Implement a "sensitivity" setting, a mask, and an option to only distinguish air vs blocks to blendball (#1832) --- .../core/command/tool/brush/BlendBall.java | 87 +++++++++++++++++-- .../core/function/mask/CachedMask.java | 26 +++++- .../worldedit/command/BrushCommands.java | 16 +++- 3 files changed, 115 insertions(+), 14 deletions(-) diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/tool/brush/BlendBall.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/tool/brush/BlendBall.java index 358eda0a5..8daf5553e 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/tool/brush/BlendBall.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/tool/brush/BlendBall.java @@ -1,5 +1,7 @@ package com.fastasyncworldedit.core.command.tool.brush; +import com.fastasyncworldedit.core.function.mask.CachedMask; +import com.fastasyncworldedit.core.math.MutableBlockVector3; import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.command.tool.brush.Brush; @@ -8,10 +10,40 @@ import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockTypes; +import javax.annotation.Nullable; import java.util.Arrays; public class BlendBall implements Brush { + private static final BlockState AIR = BlockTypes.AIR.getDefaultState(); + + private final int minFreqDiff; + private final boolean onlyAir; + @Nullable private final CachedMask mask; + + /** + * Create a new {@link Brush} instance with default settings. + */ + public BlendBall() { + this(1, false, null); + } + + /** + * Create a new {@link Brush} instance. + * + * @param minFreqDiff Minimum difference between nearby blocks (3x3x3 cuboid centered on position) to alter the current block + * at a position + * @param onlyAir Only consider air for comparing existing blocks, and for altering existing blocks + * @param mask Mask to limit the blocks being considered for alteration. Will also limit blocks types able to be + * placed, and will consider blocks not meeting the mask as air + * @since TODO + */ + public BlendBall(int minFreqDiff, boolean onlyAir, @Nullable CachedMask mask) { + this.minFreqDiff = minFreqDiff; + this.onlyAir = onlyAir; + this.mask = mask; + } + @Override public void build(EditSession editSession, BlockVector3 position, Pattern pattern, double size) throws MaxChangedBlocksException { @@ -27,41 +59,75 @@ public class BlendBall implements Brush { int maxY = editSession.getMaxY(); int minY = editSession.getMinY(); + MutableBlockVector3 mutable = new MutableBlockVector3(); for (int x = -outsetSize; x <= outsetSize; x++) { int x0 = x + tx; + int xx = x * x; for (int y = -outsetSize; y <= outsetSize; y++) { int y0 = y + ty; + if (y0 + 1 < minY || y0 - 1 > maxY) { + continue; + } + int yy = y * y; + int xxyy = xx + yy; + if (xxyy >= brushSizeSquared) { + continue; + } for (int z = -outsetSize; z <= outsetSize; z++) { - if (x * x + y * y + z * z >= brushSizeSquared) { + int z0 = z + tz; + if (xxyy + z * z >= brushSizeSquared || maskFails(editSession, mutable.setComponents(x0, y0, z0))) { continue; } - int z0 = z + tz; - int highest = 1; + int highest = 1, currentBlockFrequency = 1; BlockState currentState = editSession.getBlock(x0, y0, z0); BlockState highestState = currentState; + int currentStateID = currentState.getInternalBlockTypeId(); Arrays.fill(frequency, 0); + int air = 0; + int total = 26; boolean tie = false; for (int ox = -1; ox <= 1; ox++) { for (int oz = -1; oz <= 1; oz++) { for (int oy = -1; oy <= 1; oy++) { - if (oy + y0 < minY || oy + y0 > maxY) { + if (ox == 0 && oy == 0 && oz == 0) { + continue; + } else if (oy + y0 < minY || oy + y0 > maxY) { + total--; continue; } - BlockState state = editSession.getBlock(x0 + ox, y0 + oy, z0 + oz); - int count = frequency[state.getInternalBlockTypeId()]; + boolean masked = maskFails(editSession, mutable.setComponents(x0 + ox, y0 + oy, z0 + oz)); + BlockState state = masked ? AIR : editSession.getBlock(x0 + ox, y0 + oy, z0 + oz); + if (state.getBlockType().getMaterial().isAir()) { + air++; + } + int internalID = state.getInternalBlockTypeId(); + int count = frequency[internalID]; + if (internalID == currentStateID) { + currentBlockFrequency++; + } count++; - if (count > highest) { + if (count - highest >= minFreqDiff) { highest = count; highestState = state; tie = false; } else if (count == highest) { tie = true; } - frequency[state.getInternalBlockTypeId()] = count; + frequency[internalID] = count; } } } - if (!tie && currentState != highestState) { + if (onlyAir) { + if (air * 2 - total >= minFreqDiff) { + if (!currentState.isAir()) { + editSession.setBlock(x0, y0, z0, AIR); + } + } else if (currentState.isAir() && total - 2 * air >= minFreqDiff) { + editSession.setBlock(x0, y0, z0, highestState); + } + continue; + } + if (highest - currentBlockFrequency >= minFreqDiff && !tie && currentState != highestState) { editSession.setBlock(x0, y0, z0, highestState); } } @@ -69,5 +135,8 @@ public class BlendBall implements Brush { } } + private boolean maskFails(EditSession editSession, MutableBlockVector3 mutable) { + return mask == null || !mask.test(editSession, mutable); + } } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/CachedMask.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/CachedMask.java index f0b0df48c..5c29d9323 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/CachedMask.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/CachedMask.java @@ -1,6 +1,8 @@ package com.fastasyncworldedit.core.function.mask; import com.fastasyncworldedit.core.math.BlockVectorSet; +import com.fastasyncworldedit.core.math.LocalBlockVectorSet; +import com.fastasyncworldedit.core.util.collection.BlockVector3Set; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.function.mask.AbstractExtentMask; import com.sk89q.worldedit.function.mask.Mask; @@ -11,12 +13,30 @@ import javax.annotation.Nullable; public class CachedMask extends AbstractDelegateMask implements ResettableMask { private final boolean hasExtent; - private transient BlockVectorSet cache_checked = new BlockVectorSet(); - private transient BlockVectorSet cache_results = new BlockVectorSet(); + private transient BlockVector3Set cache_checked; + private transient BlockVector3Set cache_results; public CachedMask(Mask mask) { + this(mask, false); + } + + /** + * Create a new CachedMask instance for the given mask + * + * @param mask Mask to cache results of + * @param local If the area will be small + * @since TODO + */ + public CachedMask(Mask mask, boolean local) { super(mask); hasExtent = mask instanceof AbstractExtentMask; + if (local) { + cache_checked = new LocalBlockVectorSet(); + cache_results = new LocalBlockVectorSet(); + } else { + cache_checked = new BlockVectorSet(); + cache_results = new BlockVectorSet(); + } } public static CachedMask cache(Mask mask) { @@ -95,7 +115,7 @@ public class CachedMask extends AbstractDelegateMask implements ResettableMask { @Override public Mask copy() { - return new CachedMask(getMask().copy()); + return new CachedMask(getMask().copy(), cache_checked instanceof LocalBlockVectorSet); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java index 77cb921cc..5c87d9136 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java @@ -50,6 +50,7 @@ import com.fastasyncworldedit.core.command.tool.sweep.SweepBrush; import com.fastasyncworldedit.core.configuration.Caption; import com.fastasyncworldedit.core.configuration.Settings; import com.fastasyncworldedit.core.extent.clipboard.MultiClipboardHolder; +import com.fastasyncworldedit.core.function.mask.CachedMask; import com.fastasyncworldedit.core.function.mask.IdMask; import com.fastasyncworldedit.core.function.mask.SingleBlockTypeMask; import com.fastasyncworldedit.core.limit.FaweLimit; @@ -138,6 +139,7 @@ import java.nio.file.FileSystems; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; +import static com.sk89q.worldedit.internal.command.CommandUtil.checkCommandArgument; /** * Commands to set brush shape. @@ -175,10 +177,20 @@ public class BrushCommands { public void blendBallBrush( InjectedValueAccess context, @Arg(desc = "The radius to sample for blending", def = "5") - Expression radius + Expression radius, + @Arg(desc = "Minimum difference in frequency to change block", def = "1") + int minFreqDiff, + @Switch(name = 'a', desc = "Compare only air vs existing blocks") + boolean onlyAir, + @ArgFlag(name = 'm', desc = "Mask to limit blocks being considered", def = "") + Mask mask ) throws WorldEditException { worldEdit.checkMaxBrushRadius(radius); - set(context, new BlendBall(), "worldedit.brush.blendball").setSize(radius); + checkCommandArgument(minFreqDiff >= 0 && minFreqDiff <= 26, "minFreqDiff not in range 0 <= value <= 26"); + if (mask != null && !(mask instanceof CachedMask)) { + mask = new CachedMask(mask, false); + } + set(context, new BlendBall(minFreqDiff, onlyAir, (CachedMask) mask), "worldedit.brush.blendball").setSize(radius); } @Command(