Solved World generation not behaving as expected

Discussion in 'Spigot Plugin Development' started by jetp250, May 12, 2017.

  1. So recently, I found an old project of mine, a world generator for floating island sort of things.
    First off, I re-wrote the whole thing to make it more efficient (sped it up by about 5 times), but I felt like if I can reach that with Bukkit's API, the performance could be pretty unbelievable by using direct NMS / Craftbukkit to do so.

    However, I've encountered a few problems; pretty critical ones, honestly.
    First off, it's dark all around the world. I needed night vision to really see anything; it was truly like a cave. Yes, I called 'chunk.initLighting()' after placing the blocks, just like Craftbukkit does.

    Secondly, the Y-Axis sometimes seems to be completely off; something that should be at the height of 120 at least can sometimes spawn at the very bottom of the world. And it varies there, too, while it shouldn't.

    Here's a screen to give you a better picture of what's happening in above two:
    [​IMG]
    Even though there is some light every here and there now, I believe it's because of Paper's 'random light updates' that I have enabled. You get the point, though.

    Now, neither of these were a thing when I worked with the Bukkit API. As I have absolutely no idea what could be wrong, I guess I'll give all the classes that have anything to do with the world generation. Starting from the main class where I create the world:
    Skylands.java (Generating the world here)
    BukkitIslandChunkGenerator.java (Bukkit one, working)
    IslandChunkGenerator.java (NMS one, broken)
    OctaveNoiseGenerator.java (Using this to generate the terrain itself)
    FastRandom.java (Noise and material picking done with this)
    MathHelper.java (Possible flooring / rounding / ceiling methods)

    Those classes are literally everything my plugin and the project contains.
    As I have absolutely no idea of what could be messing this up, I'd highly appreciate any help :l
    Will still be looking if I'm shifting something to the wrong direction - which may very well be possible - although I haven't gotten any IndexOutOfBoundsExceptions.

    Apologies for the amount of code; if I had to guess I'd say the problem is in IslandChunkGenerator or Skylands.java, since the Noise should be working just fine.

    Thanks!

    EDIT: The world generation may or may not be hard to understand, I'll gladly explain if you have questions regarding anything in any class. The characters represent blocks, i.e '\u0020' represents Grass. I generated those values with the following:
    Code (Java):
    final Scanner scanner = new Scanner(System.in);
    while (scanner.hasNextLine()) {
        final String line = scanner.nextLine();
        final Material material;
        byte data = 0;
        final int index;
        if ((index = line.indexOf(' ')) != -1) {
            final String type = line.substring(0, index);
            try {
                material = Material.valueOf(type.toUpperCase());
                data = Byte.valueOf(line.substring(type.length() + 1));
            } catch (Exception e) {
                continue;
            }
        } else {
            try {
                material = Material.valueOf(line.toUpperCase());
            } catch (Exception e) {
                continue;
            }
        }
        if (material != null) {
            // This line
            System.out.println("'\\u"
                    + Integer.toHexString((char) (material.getId() << 4 | data) | 0x10000).substring(1) + "'");
        }
    }
    scanner.close();

    EDIT 2: Okay, switched to a more readable generation method; now using IBlockDatas instead of characters:
    Code (Java):
    @Override
    public Chunk getOrCreateChunk(final int chunkX, final int chunkZ) {
        this.random.setSeed(chunkX * 341873128712L + chunkZ * 132897987541L);
        final Chunk chunk = new Chunk(this.world, chunkX, chunkZ);
        final ChunkSection[] sections = chunk.getSections();
        final int rX = chunkX * 16;
        final int rZ = chunkZ * 16;
        for (int x = 0; x < 16; ++x) {
            final int realX = rX + x;
            for (int z = 0; z < 16; ++z) {
                final int realZ = rZ + z;
                final float bottom = Math.max(0, noise.noise(realX, realZ, 1.4F, 0.81F) * 32 + 90);
                float top = noise.noise(realX, realZ, 0.2F, 0.1F) * 14 + 90;
                final float sub = top - bottom;
                if (sub < 50F) {
                    top -= (50F - sub) * 0.02F;
                }
                if (top - bottom >= 3) {
                    int y = top >= 255 ? 255 : MathHelper.ceil(top);
                    final int decorationHeight = y - 3;
                    int sec;
                    if (sections[sec = y >> 4] == null)
                        sections[sec] = new ChunkSection(sec, true);
                    sections[sec].getBlocks().setBlock(x, y & 0xF, z, Blocks.GRASS.getBlockData());
                    for (; y > decorationHeight; --y) {
                        if (sections[sec = y >> 4] == null)
                            sections[sec] = new ChunkSection(sec, true);
                        sections[sec].getBlocks().setBlock(x, y & 0xF, z, Blocks.DIRT.getBlockData());
                    }
                    for (; y > bottom; --y) {
                        if (sections[sec = y >> 4] == null)
                            sections[sec] = new ChunkSection(sec, true);
                        sections[sec].getBlocks().setBlock(x, y & 0xF, z, Blocks.STONE.getBlockData());
                    }
                }
            }
        }
        chunk.initLighting();
        return chunk;
    }
    Generating is lightning-fast, but still has the two same problems. Random chunk errors (generating at the bottom) and no lighting.

    EDIT 3: Solved, should've used 'sec << 4' instead of 'sec' for the Y-Coord of the ChunkSections. Lighting now works fine, no chunk errors...

    But.
    Now the generation takes about 10x more, making it just as slow as the one I made with Bukkit API.
    Why is that? Can I optimize this somehow? Marking as solved anyways.
     
    #1 jetp250, May 12, 2017
    Last edited: May 12, 2017
  2. I think your initial assumption that bypassing Bukkit is faster, is wrong. That part of the API maps pretty directly to the underlying code. The best way to verify that is by timing it. Use both generators to generate a certain amount of chunks and set them up so they both output a bukkit World object, then time how long each take.

    One optimization I can suggest in your code is to only lookup the chunk section once. I don't think that will make much difference as it's probably already optimized away by the compiler. Here's my version of IslandChunkGenerator::getOrCreateChunk:
    Code (Java):
    public Chunk getOrCreateChunk(final int chunkX, final int chunkZ) {
        Random rand = new Random();
        final Chunk chunk = new Chunk(this.world, chunkX, chunkZ);

        final char[][] charSections = new char[16][];
        charSections[0] = new char[4096];

        for (int x = 0; x < 16; ++x) {
            final int realX = chunkX * 16 + x;
            for (int z = 0; z < 16; ++z) {
                final int realZ = chunkZ * 16 + z;
                final int bottom = (int) Math.max(0, noise.noise(realX, realZ, 1.4F, 0.81F) * 40 + 90);
                final int top = (int) Math.min(255, noise.noise(realX, realZ, 0.2F, 0.1F) * 14 + 90);
                if(bottom > top) {
                    continue;
                }
                for(int y = bottom; y < top; ++y) {
                    int sectionIndex = y / 16;
                    char[] charSection = charSections[sectionIndex];
                    if (charSection == null) {
                        charSection = new char[4096];
                        charSections[sectionIndex] = charSection;
                    }
               
                    char blockId;
                    int blocksUntilTop = top - y;
                    if(blocksUntilTop == 1)
                        blockId = '\u0020';
                    else if(blocksUntilTop >= 2 && blocksUntilTop <= 2)
                        blockId = '\u0030';
                    else {
                        blockId = STONE[rand.nextInt(STONE.length)];
                    }
               
                    int blockIndex = (y & 0xF) << 8 | z << 4 | x;
                    charSection[blockIndex] = blockId;
                }
            }
        }

        final ChunkSection[] sections = chunk.getSections();
        for (int s = 0; s < 16; ++s) {
            final char[] section = charSections;
            if (section != null) {
                sections = new ChunkSection(s * 16, true, section);
            }
        }

        final byte[] biomeIndex = chunk.getBiomeIndex();
        final byte forest = (byte) (BiomeBase.REGISTRY_ID.a(Biomes.f) & 0xFF);
        for (int i = 0; i < biomeIndex.length; ++i) {
            biomeIndex = forest;
        }
        chunk.initLighting();
        return chunk;
    }

    Also, you might've noticed you see a lot more stone data 0 than the other ones. That's because your FastRandom nextInt(bound) doesn't account for modulo bias.

    A tip: I always use MCP when fiddling around with nms. The source of your problem is actually documented in mcp : http://i.imgur.com/wnh2l84.png
     
  3. Thanks for the reply! And yes, as I edited, I had forgotten to shift the section and thus gave the wrong Y; which fixed it.
    Also, I did realize it directly ports it to nms, however; by doing it this way, I avoided a loop where it would've turned the Bukkit block ID's to nms ones as I did it directly. One loop less & way less lookups.

    Now then, using characters also does some math trickery & registry lookupping, and at the end just calls DataPaletteBlock.setBlock() which I then moved to on my edit. Not only is it more readable that way, but a tiny bit more efficient.

    With my current setup, generation takes from 0ms to 0 ms (with an avg. of 0ms), and I'm pretty happy with it. I don't really want to move back to using characters and that kind of stuff, although I do understand why you're using them - I forgot to update the code in the pastebin links. My bad.

    Now, prepare for explanations about immature optimizations and why I do certain things differently.

    It looks like you're avoiding a lot of bitwise shifting and all that, but after all you're doing one more IF-statement and have more math trickery compared to mine. May not look like it, but I avoid the if's by setting the top block manually (no loop), then looping through the 'decoration height' whilst decreasing Y on every block, and after that, looping from the remaining Y to the bottom. This way I needn't to check for what specific block it should set there. I have only one check for each block; I'm only checking if the 'ChunkSection' is null and if it is, creating it. At the same time, with the same values, I'm saving the 'ChunkSection' to avoid one array lookup, and the shifted 'sec' for creating the new ChunkSection, so I only have one shift and one array access (two when creating the new ChunkSection). Then of course, I'm shifting the Y to right by four (because that results in a multiply of 4, while directly using Y doesn't; although the Y works too).

    Now then, about the FastRandom, I'm aware it has a higher chance for 0, yes. That doesn't really matter for this and many other cases though, I don't think anybody will notice if there's a few more blocks of stone; and as long as I'm not creating any betting-kind of plugin where the chances have to be equal, it pretty much doesn't matter. But why are you creating a new Random for each chunk? Assuming that was just for the sake of explanation.

    Also, about the biome fuss I had below the generation; that was completely useless. I thought it'd have to be filled (don't ask me why) so I did fill it - with one biome. However, after removing that part, it seems it generated the vanilla-like biomes normally.

    Don't worry, I don't code everything like this; I have a feeling I'm breaking many java conventions here; I just want to see how fast can I get it if I really do my best - even with the smallest immature optimizations that nobody should care about.

    Now porting the project to GitHub and updating the post with a link to it - but, of course - keeping the 'broken' pastebin ones for future reference. Not sure if this'll help anybody though.