3. Tips and advanced things

Using the client is pretty simple to use once you understand how things work. Though there might be a few things that need some clarification, this is what this section is for.

Handling the errors

Regardless of whether you use blocking or non-blocking strategy to interact with the Geometry Dash API, handling errors properly is something important. To help with that, each method of AnonymousGDClient and AuthenticatedGDClient specifies all kinds of errors in their javadoc that may happen when calling them. In a general way, any call of any of the methods of the clients may produce those 3 errors:

Besides those, additional errors may be listed for specific methods. For example, if you look at the method getDailyLevel(), you can see that it may also emit a NoTimelyAvailableException, which happens when no Daily level or Weekly demon is available. Additionally, if you have configured a request timeout when building the client, it can be handled by intercepting a TimeoutException.

The GDPaginator class

When a request may return several results, the method corresponding to the request will return a GDPaginator. A GDPaginator contains info on how many elements were found, how many pages there are, how many elements there are per page, and a List containing the actual elements for the current page. If you try to print a GDPaginator instance, the toString() method will give an output similar to this:

GDPaginator [
    Page 1 of 1139, showing 10 elements out of 11389
    Has next page: true ; Has previous page: false
    Elements: [GDLevel [name=Dragon Slayer, creatorID=8599996, creatorName=Subw00fer, description=Storm the castle, slay the dragon, save the "princess". Simple enough., difficulty=NORMAL, demonDifficulty=MEDIUM, stars=10, featuredScore=24870, isEpic=true, downloads=99773, likes=8723, length=LONG, coinCount=1, hasCoinsVerified=true, levelVersion=4, gameVersion=21, objectCount=65535, isDemon=true, isAuto=false, originalLevelID=0, requestedStars=10], GDLevel [name=Jayuff, creatorID=9261799, creatorName=Desumari, description=Simple level for Jayuff. Enjoy ;}, difficulty=HARD, demonDifficulty=HARD, stars=5, featuredScore=24862, isEpic=true, downloads=100408, likes=13283, length=LONG, coinCount=0, hasCoinsVerified=true, levelVersion=2, gameVersion=21, objectCount=40920, isDemon=false, isAuto=false, originalLevelID=0, requestedStars=5], GDLevel [name=ARRE Robot, creatorID=7306518, creatorName=izhar, description=Podras montar a este revoltoso robot hasta la victoria? (Monedas y Contra en mi canal de YT), difficulty=NORMAL, demonDifficulty=MEDIUM, stars=10, featuredScore=24860, isEpic=true, downloads=145430, likes=14767, length=LONG, coinCount=3, hasCoinsVerified=true, levelVersion=5, gameVersion=21, objectCount=19571, isDemon=true, isAuto=false, originalLevelID=0, requestedStars=10], GDLevel [name=Bliss, creatorID=2801674, creatorName=Yendis, description=A new level where I experimented a bit with the deco. Enjoy :), difficulty=INSANE, demonDifficulty=EXTREME, stars=8, featuredScore=24852, isEpic=true, downloads=42833, likes=5523, length=LONG, coinCount=0, hasCoinsVerified=true, levelVersion=5, gameVersion=21, objectCount=20082, isDemon=false, isAuto=false, originalLevelID=0, requestedStars=8], GDLevel [name=Shimmering, creatorID=41705440, creatorName=DashTy, description=Yeah Boi ! Hope you like this new level :D ! Layout By Ploid ! :) ( Pass on my YT Channel ) My best ! Enjoy :3, difficulty=HARDER, demonDifficulty=INSANE, stars=6, featuredScore=24852, isEpic=true, downloads=114616, likes=13186, length=LONG, coinCount=3, hasCoinsVerified=true, levelVersion=5, gameVersion=21, objectCount=21605, isDemon=false, isAuto=false, originalLevelID=0, requestedStars=6], GDLevel [name=Dismantle, creatorID=8490812, creatorName=Carnage37, description=For the one and only Pauze, Thanks., difficulty=INSANE, demonDifficulty=EXTREME, stars=8, featuredScore=24851, isEpic=true, downloads=21569, likes=2548, length=LONG, coinCount=3, hasCoinsVerified=true, levelVersion=2, gameVersion=21, objectCount=52522, isDemon=false, isAuto=false, originalLevelID=0, requestedStars=8], GDLevel [name=SYNT, creatorID=4765055, creatorName=ZaDoXXZl, description=Dedicated to team Uniqueminds and Watermelon.My inspiration from always Thomartin :). After a 1 month.Enjoy;), difficulty=HARDER, demonDifficulty=INSANE, stars=6, featuredScore=24851, isEpic=true, downloads=22126, likes=3040, length=LONG, coinCount=3, hasCoinsVerified=true, levelVersion=1, gameVersion=21, objectCount=51001, isDemon=false, isAuto=false, originalLevelID=0, requestedStars=0], GDLevel [name=Nano, creatorID=10652115, creatorName=X trailz, description=[Inspired on the "byte's" series by Triaxis :P], difficulty=HARDER, demonDifficulty=INSANE, stars=6, featuredScore=24851, isEpic=true, downloads=193921, likes=20979, length=LONG, coinCount=2, hasCoinsVerified=true, levelVersion=2, gameVersion=21, objectCount=57405, isDemon=false, isAuto=false, originalLevelID=0, requestedStars=6], GDLevel [name=Minimal Madness, creatorID=4156015, creatorName=Plygon, description=This level took months and months to make! I hope you like it!, difficulty=INSANE, demonDifficulty=EXTREME, stars=8, featuredScore=24851, isEpic=true, downloads=36850, likes=4761, length=LONG, coinCount=3, hasCoinsVerified=true, levelVersion=2, gameVersion=21, objectCount=23471, isDemon=false, isAuto=false, originalLevelID=0, requestedStars=6], GDLevel [name=AscenT, creatorID=43222828, creatorName=GeomTer, description=I hope you like this new level a collab with AZYRE and me  Insane 8* :), difficulty=INSANE, demonDifficulty=EXTREME, stars=8, featuredScore=24850, isEpic=false, downloads=17797, likes=2232, length=LONG, coinCount=2, hasCoinsVerified=true, levelVersion=1, gameVersion=21, objectCount=47392, isDemon=false, isAuto=false, originalLevelID=50306607, requestedStars=8]]
]

You can see all information that a GDPaginator can hold. In the code, you have access to this information. In this example, let's say you are browsing levels on GD:

int pageNumber = paginator.getPageNumber(); // The current page number
int maxSizePerPage = paginator.getMaxSizePerPage(); // Max number of elements per page
int totalSize = paginator.getTotalSize(); // Total number of elements found
boolean hasNextPage = paginator.hasNextPage(); // Whether you can go to next page
boolean hasPreviousPage = paginator.hasPreviousPage(); // Whether you can go to previous page
int totalNumberOfPages = paginator.getTotalNumberOfPages(); // The total number of pages
List<GDLevel> elements = paginator.asList(); // Get the list of elements on the current page
Mono<GDPaginator<GDLevel>> nextPage = paginator.goToNextPage(); // Returns a new GDPaginator containing elements on the next page
Mono<GDPaginator<GDLevel>> previousPage = paginator.goToPreviousPage(); // Returns a new GDPaginator containing elements on the next page
Mono<GDPaginator<GDLevel>> page6 = paginator.goTo(6); // Returns a new GDPaginator containing elements on the 6th page

Since GDPaginator implements Iterable, you can loop through all elements easily using the syntax:

for (GDLevel element : paginator) {
    // code
}

which is strictly equivalent to:

List<GDLevel> elements = paginator.asList();
for (GDLevel element : elements) {
    // code
}

One important thing to know is that in the code, all page numbers are zero-indexed. First page is page 0, second page is page 1, and so on. The toString() method aim to provide a more user-friendly way to display things, that's why page numbers start at 1 there.

Lazy properties

When you request some things using the client, sometimes the client won't give you everything at once. This is the case when getting a specific information on an entity requires an extra request to the GD servers. For instance, for a GDLevel, you won't have any issues getting the level ID, the level name, or the description.

long id = level.getId();
String name = level.getName();
String description = level.getDescription();

But what if you want to get information on the song that the level uses?

Mono<GDSong> = level.getSong();

Yup, it doesn't return the GDSong directly, but a Mono of it. This is because there are some cases where song info is not given along with the level data. That's why it would need to make another request to get song info. To get the value of the song, it's done by the same way, either you block() it, or you define a callback then subscribe() it, as you would do for a normal request.

Though, if you want nothing but the song, you can use flatMap to discard level info and only extract song info:

GDSong song = client.getLevelById(id)
        .flatMap(GDLevel::getSong)
        .block();

Generating user icons

Since JDash v3.1.0, it is possible to generate user icons into an image format manipulable with Java Graphics APIs. In this example, we are fetching RobTop's profile, generating his icons and saving then in a .png format in your system's temporary directory:

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

import jdash.jdash.client.AnonymousGDClient;
import jdash.jdash.client.GDClientBuilder;
import jdash.jdash.common.entity.GDUser;
import jdash.jdash.common.entity.IconType;
import jdash.jdash.graphics.exception.SpriteLoadException;
import jdash.graphics.SpriteFactory;
import jdash.jdash.util.GDUserIconSet;

public final class GraphicsTestMain {

    public static void main(String[] args) throws SpriteLoadException {
        // Build the client
        AnonymousGDClient client = GDClientBuilder.create().buildAnonymous();
        // Fetch RobTop's profile
        GDUser user = client.searchUser("RobTop").block();
        // Instanciate the SpriteFactory
        SpriteFactory sf = SpriteFactory.create();
        // Read the user's icon set
        GDUserIconSet iconSet = new GDUserIconSet(user, sf);
        // For each existing icon type (cube, ship, UFO, etc), generate the image
        BufferedImage mainIcon = iconSet.generateIcon(user.getMainIconType());
        BufferedImage cube = iconSet.generateIcon(IconType.CUBE);
        BufferedImage ship = iconSet.generateIcon(IconType.SHIP);
        BufferedImage ball = iconSet.generateIcon(IconType.BALL);
        BufferedImage ufo = iconSet.generateIcon(IconType.UFO);
        BufferedImage wave = iconSet.generateIcon(IconType.WAVE);
        BufferedImage robot = iconSet.generateIcon(IconType.ROBOT);
        BufferedImage spider = iconSet.generateIcon(IconType.SPIDER);
        // Save the images
        savePNG(mainIcon, "RobTop-Main");
        savePNG(cube, "RobTop-Cube");
        savePNG(ship, "RobTop-Ship");
        savePNG(ball, "RobTop-Ball");
        savePNG(ufo, "RobTop-Ufo");
        savePNG(wave, "RobTop-Wave");
        savePNG(robot, "RobTop-Robot");
        savePNG(spider, "RobTop-Spider");
    }

    /**
     * Utility method to save an image into a PNG format in the system's temporary directory.
     */
    private static void savePNG(BufferedImage img, String name) {
        try {
            String path = System.getProperty("java.io.tmpdir") + File.separator + name + ".png";
            ImageIO.write(img, "png", new File(path));
            System.out.println("Saved " + path);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}