Rosewood-Development / RoseStacker

A next-generation stacker plugin for Spigot and Paper servers
Other
139 stars 36 forks source link

API Question: getStackedBlocks() #27

Closed tastybento closed 3 years ago

tastybento commented 3 years ago

I'm the author of BentoBox/BSkyBlock and I had a request to add RoseStacker support to the Level addon that counts up blocks and gives the island a score. I usually try and do this as much async as possible to avoid lag. I grab chunk snapshots and then check each block's type. I'm looking at the API and I see getStackedBlocks() to get all the loaded stacked blocks on the server.

I have some questions:

a) What does "loaded" in this context mean? Are all stacked blocks always loaded, or do you unload blocks dynamically somehow? b) I assume it gives me a map of all stackers in every world? c) Is it async safe? If I request it async, will it work? Or must it be done sync?

Esophose commented 3 years ago

a). Loaded means all stack data in loaded chunks will be returned. Only stacks in loaded chunks are cached, the rest is stored in a database and is loaded within a few ticks after a chunk loads. b). Yes. c). Yes, it is async safe. To my knowledge the whole API is.

I should make the following comments because it seems like fairly useful information. The current released version of RoseStacker is v1.2.8 which stores all the stack data (block stacks included) in the database and has an API method of getChunkStackData() which is capable of fetching data async using a CompletableFuture. The newer version of v1.3.0 (coming soon) instead stores the block/spawner stack data in the chunk's PersistentDataContainer and loads synchronously with the chunk. The formerly mentioned API method has been deprecated since the data no longer can be fetched from the database. If the chunk is loaded, the data will be available, otherwise it will not be. I don't think it will be possible to use a ChunkSnapshot for that, but if the ChunkSnapshot also exposed the PersistentDataContainer then I can probably make an aPI method for it. A replacement to the deprecated method is getStackedBlocks(Collection) which basically does the same as the parameterless version except filters it to only contain data in the chunk's you specify. I might be able to make a pull request to the Level addon if you find this a bit too much of a struggle to figure out. Feel free to ask any other questions you may have.

tastybento commented 3 years ago

That's useful information. I read the code and it seems that all the methods eventually end up at StackingThread, which can be got by the world so I decided to focus on that. Here's the code I came up with:

////// In the constructor - get the Stacking Threads for each active world
        // RoseStacker - get threads for worlds
        if (addon.isRoseStackersEnabled()) {
            worlds.forEach((k,v) -> stackingThreads.put(k, RoseStackerAPI.getInstance().getStackingThread(v)));
        }
////////

    /**
     * Get a chunk async
     * @param env - the environment
     * @param x - chunk x coordinate
     * @param z - chunk z coordinate
     * @return a future chunk or future null if there is no chunk to load, e.g., there is no island nether
     */
    private CompletableFuture<Chunk> getWorldChunk(Environment env, int x, int z) {
        if (worlds.containsKey(env)) {
            CompletableFuture<Chunk> r2 = new CompletableFuture<>();
            Util.getChunkAtAsync(worlds.get(env), x, z, true).thenAccept(chunk -> roseStackerCheck(r2, chunk, env, x, z));
            return r2;
        }
        return CompletableFuture.completedFuture(null);
    }

    private void roseStackerCheck(CompletableFuture<Chunk> r2, Chunk chunk, Environment env, int x, int z) {
        // If the EnumMap has the stacking thread for this environment, then process it
        if (stackingThreads.containsKey(env)) {
            // Filter out this chunk from the StackingThread - now the chunk is loaded, it should be in there?
            ((StackingThread) stackingThreads.get(env)).getStackedBlocks().entrySet().stream()
            .filter(e -> e.getKey().getX() >> 4 == x && e.getKey().getZ() >> 4 == z)
            .forEach(e -> {
                // Blocks below sea level can be scored differently
                boolean belowSeaLevel = seaHeight > 0 && e.getKey().getY() <= seaHeight;
                // Check block once because the base block will be counted in the chunk snapshot
                for (int _x = 0; _x < e.getValue().getStackSize() - 1; _x++) {
                    checkBlock(e.getKey().getType(), belowSeaLevel);
                }
            });
        }
        r2.complete(chunk);
    }

To explain, there is an iterator that is stepping through all the chunk coordinates (x,z) that need to be scanned for the various environments available (over world, nether, end). The chunk is loaded async using Paper's API and then usually it would be passed back to be snapshotted and scanned. However, in this case, I run a check on what stacked blocks are available in that chunk by calling the new roseStackerCheck method. My assumption is that by loading the chunk, the stack data should be available via the StackingThread, no matter how that is stored (database or the new chunk storage). Would that be correct? I haven't actually been able to test this right now because I'm on 1.17 (actually 1.17.1) and the stacking didn't seem to be working.

Incidentally, I had a funny experience where I couldn't understand how to use your plugin. I placed a block and then tried some commands. I managed to obtain a stick and hit the block but it didn't seem to do anything. I looked on the Spigot page, watched the gifs, and tried to read the wiki for how to stack blocks, but there were no instructions. I had a stack in my inventory so I tried to drop all of them, but they just dropped as items. Then as a last resort I tried placing a block on top of another block and it disappeared from my inventory! It was a bit of a duh moment, as I assume most people get it. I just didn't realize to stack blocks you literally just stack them...

Esophose commented 3 years ago

It can take several ticks before the stack data is loaded after a chunk loads, so that may not work properly in the currently released version but should work fine in the next one.

The way you stack blocks is to literally just right click an existing block and it sucks the item in your hand into it basically. The reason it didn't look like it was doing anything is likely because you don't have HolographicDisplays or a similar hologram plugin installed, if you check the startup logs for RoseStacker it will say you need one in order to see the spawner and block stack holograms. Alternatively you can shift right click an existing block stack and it opens a GUI where you can deposit blocks instead.

Edit: it's also important to note that you probably shouldn't be caching the stacking threads yourself. If the /rs reload command is run they all get destroyed and recreated.

tastybento commented 3 years ago

It looks like 1.3.0 is out now so I'll start working on this again. Has anything changed from what we discussed?

Esophose commented 3 years ago

As of 1.3.0 block and spawner stacks now load synchronously with chunk loads, so you can just use RoseStackerAPI.getInstance().getStackedBlocks(Collection<Chunk>) and getStackedSpawners(Collection<Chunk>) to get all stacked blocks/spawners in a given collection of chunks. Alternatively what you already gave as a code example is fine, just see my last message on what issues you could potentially have by storing stacking threads like that.