Captain-Chaos / WorldPainter

WorldPainter is an interactive graphical map generator for the indie game Minecraft.
http://www.worldpainter.net/
GNU General Public License v3.0
355 stars 58 forks source link

Random water placement along void layer #37

Closed creativitRy closed 7 years ago

creativitRy commented 7 years ago

If you use the void layer and a ground cover layer with only one block (9: stationary water), and the two layers intersect (but doesn't overlap), water gets randomly placed in the void along the edges. Bug replicable in WorldPainter version 2.3.0.

https://gyazo.com/5033617ad0cd8361e9b00631ac1b8dec

Captain-Chaos commented 7 years ago

Thanks for the report! Could you elaborate a little on what you mean by "intersecting but not overlapping" and/or attach a .world file demonstrating the problem?

creativitRy commented 7 years ago

By "intersecting but not overlapping", I meant that the two layers touch each other but don't occupy the same coordinates. For example, I drew the void layer, and I drew the water layer with a filter except on void.

Here's a world file with the bug. It's not the one on the picture, but the same bug is visible. http://www.mediafire.com/file/s87dblmg2ewbqru/Bug.world

Here are the settings for the water ground cover layer. https://gyazo.com/5f2d50a1e3a31e5d0355a41c609e52c2

creativitRy commented 7 years ago

I tested it some more, and it seems as though the bug only appears when the width is set to 2 or higher. Also, both stationary water and stationary lava cause the bug.

creativitRy commented 7 years ago

I didn't look through your code completely, but judging from VoidExporter.java class's processEdgeColumn method, I believe the bug is happening due to processEdgeColumn detecting water placed by itself. You might want to store all the coordinates where the water will be placed into a collection and process them later.

creativitRy commented 7 years ago

I believe this modification to VoidExporter.java fixes it. Feel free to use (or ignore) this code without mentioning me.

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.pepsoft.worldpainter.layers.exporters;

import org.pepsoft.util.MathUtils;
import org.pepsoft.util.PerlinNoise;
import org.pepsoft.worldpainter.Dimension;
import org.pepsoft.worldpainter.exporting.AbstractLayerExporter;
import org.pepsoft.worldpainter.exporting.Fixup;
import org.pepsoft.worldpainter.exporting.MinecraftWorld;
import org.pepsoft.worldpainter.exporting.SecondPassLayerExporter;
import org.pepsoft.worldpainter.layers.Void;

import java.awt.*;
import java.util.List;
import java.util.ArrayDeque;

import static org.pepsoft.minecraft.Constants.BLK_STATIONARY_LAVA;
import static org.pepsoft.minecraft.Constants.BLK_STATIONARY_WATER;
import static org.pepsoft.minecraft.Material.AIR;
import static org.pepsoft.worldpainter.Constants.SMALL_BLOBS;

/**
 * This exporter does the second half of the void processing. The first half
 * has been performed in the first pass by hardcoded code which has left any
 * columns marked as Void completely empty.
 * 
 * <p>This plugin does some decoration around the void areas.
 * 
 * @author pepijn
 */
public class VoidExporter extends AbstractLayerExporter<org.pepsoft.worldpainter.layers.Void> implements SecondPassLayerExporter {
    public VoidExporter() {
        super(Void.INSTANCE);
    }

    @Override
    public List<Fixup> render(Dimension dimension, Rectangle area, Rectangle exportedArea, MinecraftWorld minecraftWorld) {
        if (noise.getSeed() != (dimension.getSeed() + SEED_OFFSET)) {
            noise.setSeed(dimension.getSeed() + SEED_OFFSET);
        }
        //if value is true, it is lava
        ArrayDeque<FluidEntry> fluids = new ArrayDeque<>();
        for (int x = area.x; x < area.x + area.width; x++) {
            for (int y = area.y; y < area.y + area.height; y++) {
                if (dimension.getBitLayerValueAt(Void.INSTANCE, x, y)
                        && (dimension.getDistanceToEdge(Void.INSTANCE, x, y, 2) < 2)) {
                    // We're on the edge of the Void
                    processEdgeColumn(dimension, x, y, fluids, minecraftWorld);
                }
            }
        }
        processFluids(fluids, minecraftWorld);
        return null;
    }

    private void processEdgeColumn(final Dimension dimension, final int x, final int y, ArrayDeque<FluidEntry> fluids, final MinecraftWorld minecraftWorld) {
        final int maxHeight = minecraftWorld.getMaxHeight();
        // Taper the world edges slightly inward
        final int r = 3;
        for (int dx = -r; dx <= r; dx++) {
            for (int dy = -r; dy <= r; dy++) {
                if ((dx != 0) || (dy != 0)) {
                    final int x2 = x + dx, y2 = y + dy;
                    final float distance = MathUtils.getDistance(dx, dy);
                    final float height = dimension.getHeightAt(x2, y2);
                    final int depth = (int) (height / Math.pow(2, distance + noise.getPerlinNoise(x2 / SMALL_BLOBS, y2 / SMALL_BLOBS)) + 0.5f);
                    for (int z = 0; z < depth; z++) {
                        minecraftWorld.setMaterialAt(x2, y2, z, AIR);
                    }
                }
            }
        }
        // Check for water surrounding the column; pre-render the falling water
        // column to avoid long pauses in Minecraft when the chunks are loaded
        // (but not for ceiling dimensions)
        if (dimension.getDim() >= 0) {
            for (int z = maxHeight - 1; z >= 0; z--) {
                if ((minecraftWorld.getBlockTypeAt(x - 1, y, z) == BLK_STATIONARY_WATER)
                        || (minecraftWorld.getBlockTypeAt(x, y - 1, z) == BLK_STATIONARY_WATER)
                        || (minecraftWorld.getBlockTypeAt(x + 1, y, z) == BLK_STATIONARY_WATER)
                        || (minecraftWorld.getBlockTypeAt(x, y + 1, z) == BLK_STATIONARY_WATER)) {
                    fluids.add(new FluidEntry(x, y, z, false));
                    break;
                } else if ((minecraftWorld.getBlockTypeAt(x - 1, y, z) == BLK_STATIONARY_LAVA)
                        || (minecraftWorld.getBlockTypeAt(x, y - 1, z) == BLK_STATIONARY_LAVA)
                        || (minecraftWorld.getBlockTypeAt(x + 1, y, z) == BLK_STATIONARY_LAVA)
                        || (minecraftWorld.getBlockTypeAt(x, y + 1, z) == BLK_STATIONARY_LAVA)) {
                    fluids.add(new FluidEntry(x, y, z, true));
                    break;
                }
            }
        }
    }

    private void processFluids(ArrayDeque<FluidEntry> fluids, final MinecraftWorld minecraftWorld) {
        for (FluidEntry fluid : fluids) {
            if (fluid.isLava()) {
                minecraftWorld.setBlockTypeAt(fluid.x, fluid.y, fluid.z, BLK_STATIONARY_LAVA);
                minecraftWorld.setDataAt(fluid.x, fluid.y, fluid.z, 2);
                for (int z = fluid.z - 1; z >= 0; z--) {
                    minecraftWorld.setBlockTypeAt(fluid.x, fluid.y, z, BLK_STATIONARY_LAVA);
                    minecraftWorld.setDataAt(fluid.x, fluid.y, z, 10);
                }
            }
            else {
                minecraftWorld.setBlockTypeAt(fluid.x, fluid.y, fluid.z, BLK_STATIONARY_WATER);
                minecraftWorld.setDataAt(fluid.x, fluid.y, fluid.z, 1);
                for (int z = fluid.z - 1; z >= 0; z--) {
                    minecraftWorld.setBlockTypeAt(fluid.x, fluid.y, z, BLK_STATIONARY_WATER);
                    minecraftWorld.setDataAt(fluid.x, fluid.y, z, 9);
                }
            }
        }
    }

    private final PerlinNoise noise = new PerlinNoise(0);

    private static final long SEED_OFFSET = 142644289;

    private static class FluidEntry {
        public FluidEntry(int x, int y, int z, boolean isLava) {
            this.x = x;
            this.y = y;
            this.z = z;
            this.isLava = isLava;
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }

        public int getZ() {
            return z;
        }

        public boolean isLava() {
            return isLava;
        }

        private int x;
        private int y;
        private int z;
        private boolean isLava;
    }
}
Captain-Chaos commented 7 years ago

Thanks for the detailed information. I'll look into it!

One pedantic little point: if there's no overlap it can't be intersecting. ;) Hence my confusion. I would call that e.g. "abutting".