Fixes #456
Delay command registration
WIP shatter/stencil brush
Minor FaweQueue optimization
This commit is contained in:
Jesse Boyd 2017-03-07 22:00:49 +11:00
parent eb55f5fd7d
commit b595ec2f2d
No known key found for this signature in database
GPG Key ID: 59F1DE6293AF6E1F
11 changed files with 323 additions and 55 deletions

View File

@ -121,6 +121,8 @@ public enum BBC {
BRUSH_HEIGHT_INVALID("Invalid height map file (%s0)", "WorldEdit.Brush"),
BRUSH_SMOOTH("Smooth brush equipped (%s0 x %s1 using %s2. Note: Use the blend brush if you want to smooth overhangs or caves.).", "WorldEdit.Brush"),
BRUSH_SPHERE("Sphere brush shape equipped (%s0).", "WorldEdit.Brush"),
BRUSH_SHATTER("Shatter brush shape equipped (%s0, %s1).", "WorldEdit.Brush"),
BRUSH_STENCIL("Stencil brush equipped (%s0).", "WorldEdit.Brush"),
BRUSH_LINE("Line brush shape equipped (%s0).", "WorldEdit.Brush"),
BRUSH_SPLINE("Spline brush shape equipped (%s0). Right click an end to add a shape", "WorldEdit.Brush"),
BRUSH_SPLINE_PRIMARY_2("Added position, Click the same spot to join!", "WorldEdit.Brush"),

View File

@ -572,14 +572,38 @@ public abstract class MappedFaweQueue<WORLD, CHUNK, CHUNKSECTIONS, SECTION> exte
@Override
public int getCachedCombinedId4Data(int x, int y, int z) throws FaweException.FaweChunkLoadException {
FaweChunk fc = map.getCachedFaweChunk(x >> 4, z >> 4);
int cx = x >> 4;
int cz = z >> 4;
FaweChunk fc = map.getCachedFaweChunk(cx, cz);
if (fc != null) {
int combined = fc.getBlockCombinedId(x & 15, y, z & 15);
if (combined != 0) {
return combined;
}
}
return getCombinedId4Data(x, y, z);
int cy = y >> 4;
if (cx != lastSectionX || cz != lastSectionZ) {
lastSectionX = cx;
lastSectionZ = cz;
lastChunk = ensureChunkLoaded(cx, cz);
if (lastChunk != null) {
lastChunkSections = getSections(lastChunk);
lastSection = getCachedSection(lastChunkSections, cy);
} else {
lastChunkSections = null;
return 0;
}
} else if (cy != lastSectionY) {
if (lastChunkSections != null) {
lastSection = getCachedSection(lastChunkSections, cy);
} else {
return 0;
}
}
if (lastSection == null) {
return 0;
}
return getCombinedId4Data(lastSection, x, y, z);
}
@Override

View File

@ -24,6 +24,6 @@ public class FlattenBrush extends HeightBrush {
mask = null;
}
heightMap.setSize(size);
heightMap.apply(editSession, mask, position, size, rotation, yscale, true, true);
heightMap.perform(editSession, mask, position, size, rotation, yscale, true, true);
}
}

View File

@ -48,6 +48,6 @@ public class HeightBrush implements Brush {
mask = null;
}
heightMap.setSize(size);
heightMap.apply(editSession, mask, position, size, rotation, yscale, true, false);
heightMap.perform(editSession, mask, position, size, rotation, yscale, true, false);
}
}

View File

@ -0,0 +1,135 @@
package com.boydti.fawe.object.brush;
import com.boydti.fawe.object.PseudoRandom;
import com.boydti.fawe.object.collection.LocalBlockVectorSet;
import com.sk89q.worldedit.BlockVector;
import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.MaxChangedBlocksException;
import com.sk89q.worldedit.MutableBlockVector;
import com.sk89q.worldedit.Vector;
import com.sk89q.worldedit.command.tool.brush.Brush;
import com.sk89q.worldedit.function.mask.Mask;
import com.sk89q.worldedit.function.mask.Masks;
import com.sk89q.worldedit.function.pattern.Pattern;
import com.sk89q.worldedit.function.visitor.BreadthFirstSearch;
import com.sk89q.worldedit.regions.CuboidRegion;
public class ShatterBrush implements Brush {
private final int count;
public ShatterBrush(int count) {
this.count = count;
}
@Override
public void build(EditSession editSession, final Vector position, Pattern pattern, double size) throws MaxChangedBlocksException {
// We'll want this to be somewhat circular, so the cuboid needs to fit.
int r2Radius = (int) Math.ceil(size * Math.sqrt(2));
int radius2 = (int) (Math.ceil(r2Radius * r2Radius));
Vector bot = new MutableBlockVector(position.subtract(size, size, size));
Vector top = new MutableBlockVector(position.add(size, size, size));
CuboidRegion region = new CuboidRegion(bot, top);
// We'll want to use a fast random
PseudoRandom random = new PseudoRandom();
// We don't need double precision, so use a BlockVector
BlockVector min = region.getMinimumPoint().toBlockVector();
BlockVector max = region.getMaximumPoint().toBlockVector();
// Let's keep it inside the brush radius
int dx = max.getBlockX() - min.getBlockX() + 1;
int dy = max.getBlockY() - min.getBlockY() + 1;
int dz = max.getBlockZ() - min.getBlockZ() + 1;
// We'll store the points in a set
LocalBlockVectorSet queue = new LocalBlockVectorSet();
// User could select a single block and try to create 10 points = infinite loop
// To avoid being stuck in an infinite loop, let's stop after 5 collisions
int maxFails = 5;
for (int added = 0; added < count;) {
int x = (int) (random.nextDouble() * dx) + min.getBlockX();
int z = (int) (random.nextDouble() * dz) + min.getBlockZ();
int y = editSession.getHighestTerrainBlock(x, z, 0, 255);
// Check the adjacent blocks efficiently (loops over the adjacent blocks, or the set; whichever is faster)
if (!queue.containsRadius(x, y, z, 1)) {
added++;
queue.add(x, y, z);
} else if (maxFails-- <= 0) {
break;
}
}
// Ideally we'd calculate all the bisecting planes, but that's complex to program
// With this algorithm compute time depends on the number of blocks rather than the number of points
// - Expand from each point (block by block) until there is a collision
{
// Keep track of where we've visited
LocalBlockVectorSet visited = queue;
LocalBlockVectorSet tmp = new LocalBlockVectorSet();
// Individual frontier for each point
LocalBlockVectorSet[] frontiers = new LocalBlockVectorSet[queue.size()];
// Keep track of where each frontier has visited
LocalBlockVectorSet[] frontiersVisited = new LocalBlockVectorSet[queue.size()];
// Initiate the frontier with the starting points
int i = 0;
for (Vector pos : queue) {
LocalBlockVectorSet set = new LocalBlockVectorSet();
set.add(pos);
frontiers[i] = set;
frontiersVisited[i] = set.clone();
i++;
}
// Mask
Mask mask = editSession.getMask();
if (mask == null) {
mask = Masks.alwaysTrue();
}
final Mask finalMask = mask;
// Expand
boolean notEmpty = true;
while (notEmpty) {
notEmpty = false;
for (i = 0; i < frontiers.length; i++) {
LocalBlockVectorSet frontier = frontiers[i];
notEmpty |= !frontier.isEmpty();
final LocalBlockVectorSet frontierVisited = frontiersVisited[i];
// This is a temporary set with the next blocks the frontier will visit
final LocalBlockVectorSet finalTmp = tmp;
frontier.forEach(new LocalBlockVectorSet.BlockVectorSetVisitor() {
@Override
public void run(int x, int y, int z, int index) {
if (PseudoRandom.random.random(2) == 0) {
finalTmp.add(x, y, z);
return;
}
for (Vector direction : BreadthFirstSearch.DEFAULT_DIRECTIONS) {
int x2 = x + direction.getBlockX();
int y2 = y + direction.getBlockY();
int z2 = z + direction.getBlockZ();
// Check boundary
int dx = position.getBlockX() - x2;
int dy = position.getBlockY() - y2;
int dz = position.getBlockZ() - z2;
int dSqr = (dx * dx) + (dy * dy) + (dz * dz);
if (dSqr <= radius2) {
if (finalMask.test(MutableBlockVector.get(x2, y2, z2))) {
// (collision) If it's visited and part of another frontier, set the block
if (!visited.add(x2, y2, z2)) {
if (!frontierVisited.contains(x2, y2, z2)) {
editSession.setBlock(x2, y2, z2, pattern);
}
} else {
// Hasn't visited and not a collision = add it
finalTmp.add(x2, y2, z2);
frontierVisited.add(x2, y2, z2);
}
}
}
}
}
});
// Swap the frontier with the temporary set
frontiers[i] = tmp;
tmp = frontier;
tmp.clear();
}
}
}
}
}

View File

@ -0,0 +1,54 @@
package com.boydti.fawe.object.brush;
import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.MaxChangedBlocksException;
import com.sk89q.worldedit.Vector;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.function.mask.Mask;
import com.sk89q.worldedit.function.mask.Masks;
import com.sk89q.worldedit.function.pattern.Pattern;
import java.io.InputStream;
public class StencilBrush extends HeightBrush {
private final boolean onlyWhite;
public StencilBrush(InputStream stream, int rotation, double yscale, boolean onlyWhite, Clipboard clipboard) {
super(stream, rotation, yscale, clipboard);
this.onlyWhite = onlyWhite;
}
@Override
public void build(EditSession editSession, Vector position, Pattern pattern, double sizeDouble) throws MaxChangedBlocksException {
int size = (int) sizeDouble;
Mask mask = editSession.getMask();
if (mask == Masks.alwaysTrue() || mask == Masks.alwaysTrue2D()) {
mask = null;
}
heightMap.setSize(size);
for (int x = -size; x <= size; x++) {
int xx = position.getBlockX() + x;
for (int z = -size; z <= size; z++) {
int zz = position.getBlockZ() + z;
double raise;
switch (rotation) {
default:raise = heightMap.getHeight(x, z); break;
case 1: raise = heightMap.getHeight(z, x); break;
case 2: raise = heightMap.getHeight(-x, -z); break;
case 3: raise = heightMap.getHeight(-z, -x);break;
}
raise *= yscale;
if (raise == 0 || (onlyWhite && raise < 255)) {
continue;
}
}
}
int[] data = heightMap.generateHeightData(editSession, mask, position, size, rotation, yscale, true, false);
int diameter = size * 2;
int x = position.getBlockX();
int y = position.getBlockY();
int z = position.getBlockZ();
}
}

View File

@ -118,7 +118,32 @@ public class ScalableHeightMap {
return new ArrayHeightMap(array);
}
public void apply(EditSession session, Mask mask, Vector pos, int size, int rotationMode, double yscale, boolean smooth, boolean towards) throws MaxChangedBlocksException {
public void perform(EditSession session, Mask mask, Vector pos, int size, int rotationMode, double yscale, boolean smooth, boolean towards) throws MaxChangedBlocksException {
int[] data = generateHeightData(session, mask, pos, size, rotationMode, yscale, smooth, towards);
applyHeightMapData(data, session, mask, pos, size, rotationMode, yscale, smooth, towards);
}
public void applyHeightMapData(int[] data, EditSession session, Mask mask, Vector pos, int size, int rotationMode, double yscale, boolean smooth, boolean towards) throws MaxChangedBlocksException {
Vector top = session.getMaximumPoint();
int maxY = top.getBlockY();
int diameter = 2 * size + 1;
int iterations = 1;
WorldVector min = new WorldVector(LocalWorldAdapter.adapt(session.getWorld()), pos.subtract(size, maxY, size));
Vector max = pos.add(size, maxY, size);
Region region = new CuboidRegion(session.getWorld(), min, max);
HeightMap heightMap = new HeightMap(session, region, true);
if (smooth) {
try {
HeightMapFilter filter = (HeightMapFilter) HeightMapFilter.class.getConstructors()[0].newInstance(GaussianKernel.class.getConstructors()[0].newInstance(5, 1));
data = filter.filter(data, diameter, diameter);
} catch (Throwable e) {
MainUtil.handleError(e);
}
}
heightMap.apply(data);
}
public int[] generateHeightData(EditSession session, Mask mask, Vector pos, int size, int rotationMode, double yscale, boolean smooth, boolean towards) {
Vector top = session.getMaximumPoint();
int maxY = top.getBlockY();
int diameter = 2 * size + 1;
@ -201,19 +226,6 @@ public class ScalableHeightMap {
}
}
}
int iterations = 1;
WorldVector min = new WorldVector(LocalWorldAdapter.adapt(session.getWorld()), pos.subtract(size, maxY, size));
Vector max = pos.add(size, maxY, size);
Region region = new CuboidRegion(session.getWorld(), min, max);
HeightMap heightMap = new HeightMap(session, region, true);
if (smooth) {
try {
HeightMapFilter filter = (HeightMapFilter) HeightMapFilter.class.getConstructors()[0].newInstance(GaussianKernel.class.getConstructors()[0].newInstance(5, 1));
newData = filter.filter(newData, diameter, diameter);
} catch (Throwable e) {
MainUtil.handleError(e);
}
}
heightMap.apply(newData);
return newData;
}
}

View File

@ -35,7 +35,9 @@ import com.boydti.fawe.object.brush.LineBrush;
import com.boydti.fawe.object.brush.RaiseBrush;
import com.boydti.fawe.object.brush.RecurseBrush;
import com.boydti.fawe.object.brush.SplineBrush;
import com.boydti.fawe.object.brush.StencilBrush;
import com.boydti.fawe.object.brush.TargetMode;
import com.boydti.fawe.object.brush.ShatterBrush;
import com.boydti.fawe.object.brush.heightmap.ScalableHeightMap;
import com.boydti.fawe.object.brush.scroll.ScrollClipboard;
import com.boydti.fawe.object.brush.scroll.ScrollMask;
@ -79,6 +81,7 @@ import com.sk89q.worldedit.extension.platform.CommandManager;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat;
import com.sk89q.worldedit.function.mask.BlockMask;
import com.sk89q.worldedit.function.mask.ExistingBlockMask;
import com.sk89q.worldedit.function.mask.Mask;
import com.sk89q.worldedit.function.pattern.BlockPattern;
import com.sk89q.worldedit.function.pattern.Pattern;
@ -268,7 +271,7 @@ public class BrushCommands {
File working = this.worldEdit.getWorkingDirectoryFile(config.saveDir);
File dir = new File(working, (Settings.IMP.PATHS.PER_PLAYER_SCHEMATICS ? (player.getUniqueId().toString() + File.separator) : "") + filename);
if (!dir.exists()) {
if (!filename.contains("/") && !filename.contains("\\")) {
if ((!filename.contains("/") && !filename.contains("\\")) || player.hasPermission("worldedit.schematic.load.other")) {
dir = new File(this.worldEdit.getWorkingDirectoryFile(config.saveDir), filename);
}
}
@ -507,27 +510,52 @@ public class BrushCommands {
if (!FawePlayer.wrap(player).hasPermission("fawe.tips")) BBC.TIP_BRUSH_COMMAND.or(BBC.TIP_BRUSH_RELATIVE, BBC.TIP_BRUSH_TRANSFORM, BBC.TIP_BRUSH_MASK_SOURCE, BBC.TIP_BRUSH_MASK, BBC.TIP_BRUSH_COPY, BBC.TIP_BRUSH_HEIGHT, BBC.TIP_BRUSH_SPLINE).send(player);
}
// @Command(
// aliases = { "test" },
// usage = "<pattern> [radius] [count] [distance]",
// flags = "h",
// desc = "Choose the sphere brush",
// help =
// "Chooses the sphere brush.\n" +
// "The -h flag creates hollow spheres instead.",
// min = 1,
// max = -1
// )
// @CommandPermissions("worldedit.brush.test")
// public void testBrush(Player player, LocalSession session, Pattern fill, @Optional("10") double radius, @Optional("10") int count, @Optional("10") int distance) throws WorldEditException {
// worldEdit.checkMaxBrushRadius(radius);
//
// BrushTool tool = session.getBrushTool(player);
// tool.setFill(fill);
// tool.setSize(radius);
// tool.setBrush(new Test(count), "worldedit.brush.test");
// player.print("equiped");
// }
@Command(
aliases = { "shatter", "partition", "split" },
usage = "<pattern> [radius] [count] [distance]",
desc = "Creates random lines to break the terrain into pieces",
help =
"Chooses the shatter brush",
min = 1,
max = -1
)
@CommandPermissions("worldedit.brush.shatter")
public void shatterBrush(Player player, EditSession editSession, LocalSession session, Pattern fill, @Optional("10") double radius, @Optional("10") int count) throws WorldEditException {
worldEdit.checkMaxBrushRadius(radius);
BrushTool tool = session.getBrushTool(player);
tool.setFill(fill);
tool.setSize(radius);
tool.setMask(new ExistingBlockMask(editSession));
tool.setBrush(new ShatterBrush(count), "worldedit.brush.shatter");
player.print(BBC.getPrefix() + BBC.BRUSH_SHATTER.f(radius, count));
}
@Command(
aliases = { "stencil", "color"},
usage = "<pattern> [radius] [file|#clipboard|null] [rotation] [yscale]",
desc = "Use a height map to paint a surface",
help =
"Chooses the stencil brush.\n" +
"The -w flag will only apply at maximum saturation",
min = 1,
max = -1
)
@CommandPermissions("worldedit.brush.stencil")
public void stencilBrush(Player player, LocalSession session, Pattern fill, @Optional("5") double radius, @Optional("") final String filename, @Optional("0") final int rotation, @Optional("1") final double yscale, @Switch('w') boolean onlyWhite) throws WorldEditException {
worldEdit.checkMaxBrushRadius(radius);
BrushTool tool = session.getBrushTool(player);
InputStream stream = getHeightmapStream(filename);
tool.setFill(fill);
tool.setSize(radius);
try {
tool.setBrush(new StencilBrush(stream, rotation, yscale, onlyWhite, filename.equalsIgnoreCase("#clipboard") ? session.getClipboard().getClipboard() : null), "worldedit.brush.height", player);
} catch (EmptyClipboardException ignore) {
tool.setBrush(new StencilBrush(stream, rotation, yscale, onlyWhite, null), "worldedit.brush.height", player);
}
player.print(BBC.getPrefix() + BBC.BRUSH_STENCIL.f(radius));
}
@Command(
aliases = { "cylinder", "cyl", "c" },
@ -701,11 +729,9 @@ public class BrushCommands {
terrainBrush(player, session, radius, filename, rotation, yscale, true, ScalableHeightMap.Shape.CONE);
}
private void terrainBrush(Player player, LocalSession session, double radius, String filename, int rotation, double yscale, boolean flat, ScalableHeightMap.Shape shape) throws WorldEditException {
worldEdit.checkMaxBrushRadius(radius);
private InputStream getHeightmapStream(String filename) {
String filenamePng = (filename.endsWith(".png") ? filename : filename + ".png");
File file = new File(Fawe.imp().getDirectory(), "heightmap" + File.separator + filenamePng);
InputStream stream = null;
if (!file.exists()) {
if (!filename.equals("#clipboard") && filename.length() >= 7) {
try {
@ -719,19 +745,24 @@ public class BrushCommands {
url = new URL("https://i.imgur.com/" + filenamePng);
}
ReadableByteChannel rbc = Channels.newChannel(url.openStream());
stream = Channels.newInputStream(rbc);
return Channels.newInputStream(rbc);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
} else if (!filename.equalsIgnoreCase("#clipboard")){
try {
stream = new FileInputStream(file);
return new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
return null;
}
private void terrainBrush(Player player, LocalSession session, double radius, String filename, int rotation, double yscale, boolean flat, ScalableHeightMap.Shape shape) throws WorldEditException {
worldEdit.checkMaxBrushRadius(radius);
InputStream stream = getHeightmapStream(filename);
BrushTool tool = session.getBrushTool(player);
tool.setSize(radius);
if (flat) {

View File

@ -55,6 +55,7 @@ import java.io.InputStream;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@ -132,7 +133,7 @@ public class SchematicCommands {
}
}
if (!f.exists()) {
if (!filename.contains("/") && !filename.contains("\\")) {
if ((!filename.contains("/") && !filename.contains("\\")) || player.hasPermission("worldedit.schematic.load.other")) {
dir = this.worldEdit.getWorkingDirectoryFile(config.saveDir);
f = this.worldEdit.getSafeSaveFile(player, dir, filename, format.getExtension(), format.getExtension());
}
@ -203,8 +204,13 @@ public class SchematicCommands {
final File parent = f.getParentFile();
if ((parent != null) && !parent.exists()) {
if (!parent.mkdirs()) {
log.info("Could not create folder for schematics!");
return;
try {
Files.createDirectories(parent.toPath());
} catch (IOException e) {
e.printStackTrace();
log.info("Could not create folder for schematics!");
return;
}
}
}
try {

View File

@ -18,6 +18,7 @@ import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.LocalConfiguration;
import com.sk89q.worldedit.LocalSession;
import com.sk89q.worldedit.MaxChangedBlocksException;
import com.sk89q.worldedit.MutableBlockVector;
import com.sk89q.worldedit.Vector;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.blocks.BaseBlock;
@ -232,7 +233,7 @@ public class BrushTool implements DoubleActionTraceTool, ScrollTool, MovableTool
public Vector getPosition(EditSession editSession, Player player) {
switch (targetMode) {
case TARGET_BLOCK_RANGE:
return player.getBlockTrace(getRange(), true);
return new MutableBlockVector(player.getBlockTrace(getRange(), true));
case FOWARD_POINT_PITCH: {
int d = 0;
Location loc = player.getLocation();
@ -241,7 +242,7 @@ public class BrushTool implements DoubleActionTraceTool, ScrollTool, MovableTool
d += (int) (Math.sin(Math.toRadians(pitch)) * 50);
final Vector vector = loc.getDirection().setY(0).normalize().multiply(d);
vector.add(loc.getX(), loc.getY(), loc.getZ()).toBlockVector();
return vector;
return new MutableBlockVector(vector);
}
case TARGET_POINT_HEIGHT: {
Location loc = player.getLocation();
@ -256,10 +257,10 @@ public class BrushTool implements DoubleActionTraceTool, ScrollTool, MovableTool
}
}
final int distance = (height - y) + 8;
return player.getBlockTrace(distance, true);
return new MutableBlockVector(player.getBlockTrace(distance, true));
}
case TARGET_FACE_RANGE:
return player.getBlockTraceFace(getRange(), true);
return new MutableBlockVector(player.getBlockTraceFace(getRange(), true));
default:
return null;
}

View File

@ -119,6 +119,7 @@ public final class CommandManager {
private final WorldEdit worldEdit;
private final PlatformManager platformManager;
private volatile Dispatcher dispatcher;
private volatile Platform platform;
private final DynamicStreamHandler dynamicHandler = new DynamicStreamHandler();
private final ExceptionConverter exceptionConverter;
@ -246,7 +247,9 @@ public final class CommandManager {
.registerMethods(new BrushCommands(worldEdit))
.parent().graph().getDispatcher();
if (platform != null) {
platform.registerCommands(dispatcher);
}
}
public static CommandManager getInstance() {
@ -281,7 +284,7 @@ public final class CommandManager {
}
}
platform.registerCommands(dispatcher);
this.platform = platform;
}
public void unregister() {