Skip to content

Developer Docs

Tobero edited this page Aug 5, 2023 · 27 revisions

DevDocIcon

Javadoc ToberoCat - GuiEngine Donate PayPal stars - GuiEngine forks - GuiEngine GitHub release issues - GuiEngine SpigotMain - GuiEngine

Introduction

Welcome to the developer docs! This documenation aims to cover most of usecases you'll have for this pluign. However, it's important to note that the plugin and its accompanying wiki may not be perfect. We are continuously working to improve them. If you come across any suggestions, bugs, issues, or feedback of any size, feel free to create an issue here and share your thoughts. Additionally, you can even fix it locally and submit a pull request to contribute to the project's development. Your input is highly valued as we strive to make this plugin better together.

Use it in your projects

Make sure you please with the correct version. You can find the latest one above in the releases badge or here

First, you have to get the GuiEngine builds into your project. When using maven, you can use the following:

<repositories>
  <repository>
    <id>jitpack.io</id>
    <url>https://jitpack.io</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupId>com.github.ToberoCat</groupId>
    <artifactId>GuiEngine</artifactId>
    <version><Tag></version>
    <scope>provided</scope>
  </dependency>
</dependencies>

For gradle:

repositories {
  maven { url 'https://jitpack.io' }
}

dependencies {
  implementation 'com.github.ToberoCat:GuiEngine:<Tag>'
}

You can view the javadocs here

You can now edit your plugin.yml to add GuiEngine as a dependency.

Create a GuiEngineAPI instance

GuiEngine requires you to use a own GuiEngineAPI when you want to display guis. Creating such a instance is as easy as writing a single line of code.

import org.bukkit.plugin.java.JavaPlugin;

import java.io.IOException;
import java.net.URISyntaxException;

public class MyGuiPlugin extends JavaPlugin {

    @Override
    public void onEnable() {
        GuiEngineApi guiEngineApi;
        try {
            guiEngineApi = new GuiEngineApi(this);
        } catch (IOException | URISyntaxException e) {
            getLogger().severe(e.getMessage());
            return;
        }
    }
}

The code above creates a new GuiEngineApi instance bound to this plugin. This binding to the plugin isn't necessary, as there are two other constructors. But this way, you don't have to worry about copying your guis from the resource folder into your data folder. Make sure you have a guis folder in your resources where you can put the guis your plugin needs. To see how to create your own guis, this beginner guide might help you.

If you decide to not use the constructor with the JavaPlugin, you have to create the api as follows:

import org.bukkit.plugin.java.JavaPlugin;

import java.io.File;


public class MyGuiPlugin extends JavaPlugin {

    @Override
    public void onEnable() {
        GuiEngineApi guiEngineApi = new GuiEngineApi("my-plugin", new File(getDataFolder(), "guis"),
                pathname -> {
                    return pathname.canRead();
                });
        //GuiEngineApi guiEngineApi = new GuiEngineApi("my-plugin", new File(getDataFolder(), "guis"));
    }
}

The not commeted example uses the GuiEngineApi constructor that takes the id of the api, the folder where the guis are located and a FileFilter. The ID is used to identify the api from other apis registered. The FileFilter determains what files in the folder are gui files that should get loaded by the GuiEngine.

If you use the commented approach, you can leave out the FileFilter. Then the default one will be used, where all files ending with .gui will get loaded.

Open a gui

Now that you have your GuiEngineApi instance, you can use it to open a gui. This is again very easy and straight forward. You can use the open Method provided by the GuiEngine.

GuIContext context = guiEngineApi.open(player, "my-gui");

That's all you need. Now the player will see the gui you desgined pop up on their screen. Notice the my-gui. This is actually a file in the guis folder, called my-gui.gui. When opening a gui, the extension is always stripped from it. The open method also returns you the GuiContext. This is a class containing the entire gui the user can see. You can use it to interact with the gui from the code side, but to this a little bit later.

The open method has a overloaded method too. You can pass it placeholders, which can be used in the gui. A placeholder is always indicated with: %%. The method where you don't pass any placeholders has a default placeholder, the viewer one. Let's try to replicate this with the overloaded method.

Map<String, String> placeholders = new HashMap<>();
placeholders.put("viewer", player.getUniqueId().toString());
GuiContext context = guiEngineApi.open(player, "my-gui");

In this primitive example, you can see that passing placeholders is almost as easy as opening the gui. Just create a map, populate it and pass it as method argument.

Desiging Guis without a interpreter

There is another way to open a gui for a player that doesn't involve any files, only pure code. This method can be set equal to using a regualr gui framework. I personally don't recommend it, as you should always try to use this tool to make everything configurable, but something there isn't a way around.

You will still need a gui context, aswell as a interpreter, but you can skip the file.

GuiInterpreter interpreter = guiEngineApi
        .getInterpreterManager()
        .getInterpreter("default"); // Or just use new DefaultInterpreter();
if (interpreter == null)
    return;

GuiContext context = new GuiContext(interpreter, "§eMy gui", 9, 5);
context.add(guiEngineApi, new SimpleItemComponentBuilder<>()
        .setName("§aMy Item")
        .setMaterial(Material.SLIME_BALL)
        .createComponent());

Map<String, String> placeholders = new HashMap<>();
interpreter.getRenderEngine().showGui(context, player, placeholders);

As you can see, this isn't as simple as it was before. So, let's go over this step by step. The first thing we need is a interpreter. Even though we don't have to interprete a gui from a file, we still need it to to create a gui context. For getting a gui interpreter, there are two ways. The first one is probably the easies. Just create a new instance of the interpreter you need, like I do in the commented part. This might work well for some interpreters, but especially ones that aren't included in the default guiengine plugin, you mgith prefer the second approach. You can get a instance of the interpreter manager by using getInterpreterManager() on the guiEngineApi. This then allows you to search for a interpreter, using the getInterpreter method. This is then the same id as you would use in a gui file. Because I want to use the default one, I put in "default". The interpreter returned is marked as nullable. This means there is possible the chance that this might be null. Because I know that the default interpreter is always there, I could probably skip this null check.

Now after we got the interpreter, we can create the GuiContext. This context now has the settings for the gui, like the title, width, height. Once we have the context, we can use it to add components to it. By using the add method, we can add a component. To add a component we must also pass the guiEngine api reference again. This is needed so the component can get bound to this api. Then we can start creating out component. GuiEngine aims on creating Components that are easy to make with the gui files while also being easy to create without one. That's why we a builder for each gui component. We just have to create a builder of the desired component, use the methods provided and once where're finished call the createComponent() method.

In this case I'm using the SimpleItemComponentBuilder. It's basically the builder that hides behind the type "item" in the gui files. I can then set the properties like in the gui file.

Once I've designed my gui context, I can show it to a player. This can only be done using a gui render engine. This is the thing responsible for handling events and rendering your content onto a inventory. You pass the player, the context and the placeholders. Even though the placeholders won't get parsed from any components when calling the showGui method, it's still required, so components that render their own guis inside can parse the gui with the same placeholders.

Note: You could theoretically use a different render engine then the gui interpreter provides, which might work well in some cases, but as soon as the context gets requested to be redrawn, it chooses the interpreters one, which might cause issues.

Creating custom components

Now, to avoid having to create your gui context by hand, it's a good idea to create your own compontens, you and others can use.

Component Structure

Each component has to implement in some way the GuiComponent interface. It provides methods used for rendering, event handling and serialization. Each registered component can only get register with an appropriate builder.

A component builder is a seperate interface, which should help you in creating inhertiable builders. These builders must have a deserilaize method and a createComponent.

You can think of serialization as a process, where you convert the componet into one you would be able to load from a gui file. Deserialization on the other hand takes the gui file component and converts it in something java is able to work wit - So these two things are something very important for GuiEngine. This is why most work has gone into making them easily usable.

Choosing a foundation

Creating a custom component from scratch is a lot of code. That's why it's important to know the tricks when it comes to choosing a good foundation. A bad one makes writing the component a pain in the ass, while a good one can make it a walk in the park.

That's why it's important to understand how componets are inherited:

grafik

This is the hierarchy of the GuiComponent (This might be outdated. This screenshot is from version 1.1.2. But should still be usable). As you can see, the interface GuiComponent is the root of all components. When you implement it, it gives you the finest control over your component - But this also has a bad side. The more control you get over the component's behaviour, the more code has to be written by yourself.

That's why I would recommend before you start creating your own custom component, you search for one that comes very close to your needs. This search isn't only giving the least amount of code you'll have to write, maybe the component you found provides enough for your needs, so you don't have to write your own component at all.

I'd recommend starting from the bottom of the hierarchy (This might also include components from other plugins) and slowly going up, until it's abstract enough for you to work with it. The tradeoff lies in finding the right balance between abstraction and the amount of code needed for implementation.

Create the component

After you've finally found a good base and decided that it's nessecary to create your own component, you first have to create a new class. In my example, I'll be creating a new component ontop of the SimpleItemComponent. The purpose of my component is to change it's x and y position all x seconds.

So let's start with the class RandomPositionComponent. I then extend from the SimpleItemComponent class and create the constructor calling the super constructor. I also added new constructor argument, repsonsible for setting the ticks between the new position choosen.

import io.github.toberocat.guiengine.components.provided.item.SimpleItemComponent;
import io.github.toberocat.guiengine.function.GuiFunction;
import io.github.toberocat.guiengine.render.RenderPriority;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;

import java.util.List;

/**
 * Created: 21.07.2023
 *
 * @author Tobias Madlberger (Tobias)
 */
public class RandomPositionComponent extends SimpleItemComponent {
    
    private final long ticksBetweenNewPosition;
    
    /**
     * Constructor for SimpleItemComponent.
     *
     * @param offsetX                 The X offset of the GUI component.
     * @param offsetY                 The Y offset of the GUI component.
     * @param priority                The rendering priority of the GUI component.
     * @param id                      The ID of the GUI component.
     * @param clickFunctions          The list of click functions for the GUI component.
     * @param dragFunctions           The list of drag functions for the GUI component.
     * @param closeFunctions          The list of close functions for the GUI component.
     * @param stack                   The ItemStack to be displayed in the GUI component.
     * @param hidden                  true if the GUI component is hidden, false otherwise.
     * @param ticksBetweenNewPosition Ticks that have to pass that the component randomizes its position again
     */
    public RandomPositionComponent(int offsetX,
                                   int offsetY,
                                   @NotNull RenderPriority priority,
                                   @NotNull String id,
                                   @NotNull List<GuiFunction> clickFunctions,
                                   @NotNull List<GuiFunction> dragFunctions,
                                   @NotNull List<GuiFunction> closeFunctions,
                                   @NotNull ItemStack stack,
                                   boolean hidden,
                                   long ticksBetweenNewPosition) {
        super(offsetX, offsetY, priority, id, clickFunctions, dragFunctions, closeFunctions, stack, hidden);
        this.ticksBetweenNewPosition = ticksBetweenNewPosition;
    }
}

This is my class right now. As you can see, the constructor is massive, containg a hole lot of parameters. That's why using a builder is a great option - It also allows for easier optional parameters.

Creating the builder

Now, talking about the builder, let's create one. As you know, all components registered need to have a builder. So does the SimpleItemComponent. Now create the marvelous, the mischievous, the one and only... RandomPositionComponentBuilder - Yep. That's a hell of a chunky name. This class should extend the SimpleItemComponentBuilder. But wait... what's that? What do these two arrows(<>) mean? They are here to make the builder inhertiable. When you first use a method from the parent buider, it would then return a instance of itself, remove all of your added methods. To not limit this new builder to order, I added a generic, which allows you to return a instance of the new builder. So, what should you put between those arrows? it depends - If your new component is only getting used in your plugin, because it has very specific requirements, then it's alright to just put the name of the new builder between those. If not and you're planning on creating these components so that others are able to use them as their foundation, you should make your class itself have a generic, which extends this builder's class.

I'll show both versions:

The one not desgined for being used as a foundation

public class RandomPositionComponentBuilder extends SimpleItemComponentBuilder<RandomPositionComponentBuilder> {
}

The one desgined for use as foundation:

public class RandomPositionComponentBuilder<B extends RandomPositionComponentBuilder<B>> 
        extends SimpleItemComponentBuilder<B> {
}

I'll continue using the second one, but I'll write in comments how certain things would look the other way.

Now that you have your builder, you can start adding the parameters of your constructor, that haven't been required by the parent component. In my case, it's only the ticksBetweenNewPosition. I also created a builder method to set this value. Now that you're ready to create your component, override the method createComponent. Change it's return type to your component. If you get a error, it's because your component doesn't inherite the correct parent component and vice versa for the builder.

You can now use all values of the parent builder and your own to create a new instance of the component.

package io.github.toberocat.guiengine.components.provided.random;

import io.github.toberocat.guiengine.components.provided.item.SimpleItemComponentBuilder;
import io.github.toberocat.guiengine.render.RenderPriority;
import org.jetbrains.annotations.NotNull;

/**
 * Created: 21.07.2023
 *
 * @author Tobias Madlberger (Tobias)
 */
public class RandomPositionComponentBuilder<B extends RandomPositionComponentBuilder<B>>
        extends SimpleItemComponentBuilder<B> {

    private long ticksBetweenNewPosition;

    public B setTicksBetweenNewPosition(long ticksBetweenNewPosition) {
        this.ticksBetweenNewPosition = ticksBetweenNewPosition;
        return self(); // If not using the inheritable approach, replace B with RandomPositionComponentBuilder and self() with this
    }

    @Override // Update this return value
    public @NotNull RandomPositionComponent createComponent() {
        return new RandomPositionComponent(
                x, y, RenderPriority.NORMAL, id, clickFunctions, dragFunctions, closeFunctions, 
                getItemStack(), hidden, ticksBetweenNewPosition
        );
    }
}

As you can see, the return type of the builder setter varies for the different implementation methods. When creating a instance of your component in the createComponent method, it helps looking at the code used to generate the super component, as this might give hints on how to create some arguments, like in this example, where the SimpleItemComponentBuilder has a handy method for creating the itemStack.

Deserializing the xml component

Now, to finalize this builder, you need to write the deserialize method. Just override it from the parent and leave the super call in (Unless you want to remove stuff).

In the deserialize method, you have access to the ParserContext. This class has some handy method that allow you to read properties from the component and parse them into different things. It also takes care of functions, which allow to make guis even more flexible.

Small side node: For reading the gui file, this plugin uses Jackson. You won't have something to do with it, as everything has been encapsulated in the ParserContext. But if you ever need to, you can just call the node() to receive the JsonNode.

Using the builder setter, you can parse the integer from the parser context. Where you call the super method shouldn't matter.

Finishing the component

Now that the builder has been finished, we have to tackle on the component. Currently, it has nothing special. But this will change now. As some things aren't related to GuiEngine, more to minecraft plugin development, I won't bother explaining them.

import com.fasterxml.jackson.databind.SerializerProvider;
import io.github.toberocat.guiengine.components.provided.item.SimpleItemComponent;
import io.github.toberocat.guiengine.function.GuiFunction;
import io.github.toberocat.guiengine.render.RenderPriority;
import io.github.toberocat.guiengine.utils.GeneratorContext;
import org.bukkit.Bukkit;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.util.List;
import java.util.Random;

/**
 * Created: 21.07.2023
 *
 * @author Tobias Madlberger (Tobias)
 */
public class RandomPositionComponent extends SimpleItemComponent {

    private final int taskID;
    private final long ticksBetweenNewPosition;

    /**
     * Constructor for SimpleItemComponent.
     *
     * @param offsetX                 The X offset of the GUI component.
     * @param offsetY                 The Y offset of the GUI component.
     * @param priority                The rendering priority of the GUI component.
     * @param id                      The ID of the GUI component.
     * @param clickFunctions          The list of click functions for the GUI component.
     * @param dragFunctions           The list of drag functions for the GUI component.
     * @param closeFunctions          The list of close functions for the GUI component.
     * @param stack                   The ItemStack to be displayed in the GUI component.
     * @param hidden                  true if the GUI component is hidden, false otherwise.
     * @param ticksBetweenNewPosition Ticks that have to pass that the component randomizes its position again
     */
    public RandomPositionComponent(int offsetX,
                                   int offsetY,
                                   @NotNull RenderPriority priority,
                                   @NotNull String id,
                                   @NotNull List<GuiFunction> clickFunctions,
                                   @NotNull List<GuiFunction> dragFunctions,
                                   @NotNull List<GuiFunction> closeFunctions,
                                   @NotNull ItemStack stack,
                                   boolean hidden,
                                   long ticksBetweenNewPosition) {
        super(offsetX, offsetY, priority, id, clickFunctions, dragFunctions, closeFunctions, stack, hidden);
        this.ticksBetweenNewPosition = ticksBetweenNewPosition;
        Random random = new Random();
        taskID = Bukkit.getScheduler().runTaskTimer(MyPlugin.getPlugin(MyPlugin.class), () -> {
            if (context == null)
                return;

            this.offsetX = random.nextInt(9);
            this.offsetY = random.nextInt(context.height());
            context.render();
        }, ticksBetweenNewPosition, ticksBetweenNewPosition).getTaskId();
    }

    @Override
    public void closedComponent(@NotNull InventoryCloseEvent event) {
        super.closedComponent(event);
        Bukkit.getScheduler().cancelTask(taskID);
    }

    @Override
    public void serialize(@NotNull GeneratorContext gen, @NotNull SerializerProvider serializers) throws IOException {
        super.serialize(gen, serializers);
        gen.writeNumberField("ticks-between", ticksBetweenNewPosition);
    }
}

So, I now changed added some new things. First, I created the loop. In there, I check if the context this component is bound to isn't set to null. This is because the context is only available after the constructor has been called. Then I set the offsetX and offsetY to a random position. These are the x and y positions of the component. The x is hard coded to always be between 0 - 8 and the y is always between the 0 and the gui's height.

Then I override the close method, but keep the super call in it, as looking at the sources showed me that the component I extend from uses it. In the close method I cancel the task, so the server doesn't spend time running it after the gui has been closed.

Now the counterpart to the deserialize method - The serialize method. It is used to convert the component into the xml format again. This is important for functions, like edit, because they take a component's convert it back to xml, apply the edit, convert them back and replace the old one. The method also keeps the super, as it takes care of all the other constructor values. Then I use the Generator Context, which hides the jackson JsonGenerator, to write the data.

That's all it takes to create component - Only thousands of words are needed to explain it.

Register the component

It's not over yet? Correct. You now have to register the component, else you can't use it. The registration should always be done before you create your api - If you register a component other apis might use aswell, it would be a good idea to register the components not in the onEnable of your plugin, but as soon as it gets loaded (Eg, using the PluginLoader interface provided by paper).

Now that you know where to register your components, it's time to do so. I usually prefer making a new private method for registering. I then call this method in the place where I want to register the components.

Now, the last modification needed in the RandomPositionComponent class - The Type.

public class RandomPositionComponent extends SimpleItemComponent {
    public static final @NotNull String TYPE = "random-item";
    // ... Other stuff

    @Override
    public @NotNull String getType() {
        return TYPE;
    }

This type is now responsible for telling the interpreter which builder should be used on which component.

Now, in the example below, I register the components both ways:

/**
 * Created: 21.07.2023
 *
 * @author Tobias Madlberger (Tobias)
 */
public class MyPlugin extends JavaPlugin {

    @Override
    public void onLoad() {
        registerSharedComponents();
    }

    @Override
    public void onEnable() {
        try {
            GuiEngineApi guiEngineApi = new GuiEngineApi(this);
            registerLocalComponents(guiEngineApi);
        } catch (IOException | URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

    private void registerSharedComponents() {
        GuiEngineApi.registerSharedFactory(
                RandomPositionComponent.TYPE,
                RandomPositionComponent.class,
                RandomPositionComponentBuilder.class
        );
    }

    private void registerLocalComponents(@NotNull GuiEngineApi api) {
        api.registerFactory(
                RandomPositionComponent.TYPE,
                RandomPositionComponent.class,
                RandomPositionComponentBuilder.class
        );
    }
}

The first method, registering the shared components, adds the component to every api that gets created. This should be used when you want your components to be used in all guis GuiEngine interpretes. Note that this must happen before the apis get created, therefore the onLoad call.

When you register a component directly on the api, then it's only usable for guis that get interpreted by this api.

When it comes to the registration itself, you have to provide the type, which will identify the component, the component class itself and the builder for it.

Please note, that only the type and the component class must be a unique pair. The builder can be used as often as you wish for different components.

Use the context to interact with the gui

When you create guis that should display flexible information that doesn't have a fixed size, the PagedComponent can come in quite handy. The issue: You need a way to add items to it.

This chapter is designed to bring you closer to some methods probided by the guicontext to help you overcome these issues without having to write a entire custom component.

Let's first design the gui and open it:

online-players.gui (Checkout PagedComponent Docs)

<gui title="§eOnline players" width="9" height="2">
    <component type="item" name="§ePrevious" material="DIRT">
        <on-click type="action">[container:previous]</on-click>
    </component>
    <component type="item" x="8" name="§eNext" material="DIRT">
        <on-click type="action">[container:next]</on-click>
    </component>
    <component type="paged" id="container" pattern="0,1,2,3,4,5,6,7,8" y="1" width="9" height="1"/>
</gui>
GuiEngineApi api = //... Get it somehow
GuiContext context = api.open(player, "online-players");
PagedComponent container = context.findComponentById("container", PagedComponent.class);
if (container == null)
  return; // Notify about this issue somehow

for (Player player : Bukkit.getOnlinePlayers()) {
  container.addComponent(new SimpleItemComponentBuilder<>()
    .setName(target.getName())
    .setClickFunctions(List.of(
      (api, context) -> player.sendMessage(target.getPlayerTime())
    ))
    .createComponent());
}
context.render();

Okay. Lemme explain this code. With the opening of a gui you should already be familiar. Now with the context you get you can get compontens by their id. This findComponentById method, also takes teh class of the Component, casting the GuiComponent for you. You then just have to check for null, because it might happen that someone missed out at the id of the component (Warn them about it when it happens).

Now we have the PagedComponent instance, we can use some methods it supplies. In this, we use the addComponent method, but you could also add a entire page. The added compontent is getting created with a builder (Introduced in the custom components chatper). In this case, I also added a clikc function. This takes a list of Functions. GuiEngine already provides some, like the ActionFunction we've been using for on-click events. There are also several others, like the edit, add or remove function. But I decided to to not use a premade one, but to create the functional interface myself to display the player's current time to the player when clicking on them.

Adding custom Actions

Actions are probably the easiest way to add functionality to any function. That's not just because everything supports them, it's also because they're super easy to create.

You just have to extend the Action class provided by tobero-core and override the run method with the fitting parameters. grafik

Here is a list of all actions available to you when using the default guiengine setup (Just the plugin + depend). This screenshot is from version 1.1.2, so it might be outdated.

package io.github.toberocat.guiengine.action;

import io.github.toberocat.toberocore.action.Action;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;

import java.io.File;

/**
 * Created: 21.07.2023
 *
 * @author Tobias Madlberger (Tobias)
 */
public class CreateFileAction extends Action {
    private final MyPlugin plugin;

    public CreateFileAction(MyPlugin plugin) {
        this.plugin = plugin;
    }

    @Override
    public @NotNull String label() {
        return "create-file";
    }

    @Override
    public void run(@NotNull CommandSender commandSender, @NotNull String provided) {
        new File(plugin.getDataFolder(), provided).createNewFile();
        commandSender.sendMessage("Created file");
    }
}

This is my example action. It allows you to create a file. The label is the text between the brackets ([]) that identifies the action. Now you just have to register it in your plugin's onEnable

ActionCore.register(new CreateFileAction(this));

And you're done. You can now use it like the other actions. A example:

<gui title="§eAction test" width="9" height="1">
    <component type="item" x="4" name="§eOnClick" material="DIRT">
        <on-click type="action">[create-file] hello-from-the-gui.txt</on-click>
    </component>
</gui>

You can find some more actions using more GuiEngine related stuff in here