Skip to content
stefvanschie edited this page Sep 4, 2021 · 14 revisions

Languages: Dutch (Nederlands)

You can also create your own custom panes if you feel like the current panes don't work well for the inventory you want to create. Creating Custom Panes can be tricky however, so we'll be going through how to make your own custom pane in this tutorial, step by step. You can also take a look at the default panes in this framework and see how these work.

As an example I'll be showing how to create a RandomPane. This pane allows you to add items and it will show them in a random position. So, first we create a class named RandomPane and we make it extend Pane, which is a default class for every pane.

class RandomPane extends Pane {}

Now you have to add a bunch of methods and a constructor. Just let your IDE add the methods and the constructor with the priority. For now you can just leave everything as it was generated.

So we'll need to be storing the items somewhere when you add them to the pane, so we'll be creating an array for that.

final GuiItem[] items;

Alright, so now we need to initialize the pane when we create a new Random Pane, so go to the constructor and underneath the super call (if your IDE hasn't generated this, it needs to be there) initialize the array with the size of the pane. The size of the pane is equal to the length multiplied with the height.

items = new GuiItem[length * height];

Alright now we'll be adding a new method to add one of our items and make it add it randomly into the array.

void addItem(GuiItem item) {
  items[ThreadLocalRandom.current().nextInt(items.length)] = item;
}

This will add the item somewhere randomly in our pane (it is possible for one item to override the other, but we'll just keep that in this tutorial). That's what we'll be leaving it at in terms of our own custom functionality for this tutorial, however you can of course add tons of methods to this to do whatever you like.

Now we need to tell how to render this array of items. Go over to your display method and remove the super call. Now we'll be adding the items in the correct spot in the provided inventory. Keep a few things in mind though: any item that is invisible, should not be rendered and any item outside the GUI, should not be rendered.

Alright, so we want to start by looping over every item we have in our array while remembering the index it had.

for (int i = 0; i < items.length; i++) {

}

Inside of here we want to get the item and check whether the item is okay according to the implementation details shown above.

GuiItem item = items[i];

if (item == null || !item.isVisible()) {
  continue;
}

Now we want to calculate the correct x and y coordinates based on our index, which we can do by using division and the modulo operator.

int x = index % inventoryComponent.getLength();
int y = index / inventoryComponent.getLength();

Now we want to check if these locations are inside the GUI. Keep in mind to apply the pane offset for each item.

if (x + paneOffsetX > Math.min(length, maxLength) || y + paneOffsetY > Math.min(height, maxHeight)) {
  continue;
}

Now since we've met all criteria, we can set the item in the pane.

inventoryComponent.setItem(item, getX() + x + paneOffsetX, getY() + y + paneOffsetY);

Alright, now we're done for the display part, now we need to go over to click handling. So go to the click method and remove the super call. The item stores internal data we can use to identify the item, which is what we'll be making use of here. First we have to check if we actually clicked on an item, though.

ItemStack itemStack = event.getCurrentItem();

if (itemStack == null) {
    return false;
}

By returning false we indicate that we have not found the correct item and that the caller should continue looking for one.

Now that we know our item exists, we can check whether it has the correct data. There's a built-in method for this, so we don't have to do this ourselves.

GuiItem item = findMatchingItem(Arrays.asList(items), itemStack);

Now we need to check whether the method actually found something.

if (item == null) {
    return false;
}

Now that we know we have the correct item, we can call the code attached to it and then return true.

item.callAction(event);

return true;

And that's it for our click method.

Now we have two auto-generated methods which we haven't done yet, getItems and getPanes. For getPanes, you can just return an empty list, since we don't have any panes.

return new ArrayList<>();

For getItems you want to take your item array and make it into a list.

return Arrays.asList(items);

If you'd like to have properties for your pane, such as making it orientable, flippable or rotatable, you can make your class implement the desired Orientable, Flippable or Rotatable interface.

And that's it, if you want to make your pane available with XML, follow the steps below, otherwise you're done.

View full code
class RandomPane extends Pane {

  private final GuiItem[] items;

  public RandomPane(int x, int y, int length, int height, Priority priority) {
    super(x, y, length, height, priority);

    items = new GuiItem[length * height];
  }

  @Override
  public void display(InventoryComponent inventoryComponent, int paneOffsetX, int paneOffsetY, int maxLength, int maxHeight) {
    for (int i = 0; i < items.length; i++) {
      GuiItem item = items[i];

      if (item == null || !item.isVisible()) {
        continue;
      }

      int x = index % Math.min(length, maxLength);
      int y = index / Math.min(length, maxLength);

      if (x + paneOffsetX > Math.min(length, maxLength) || y + paneOffsetY > Math.min(height, maxHeight)) {
        continue;
      }

      inventoryComponent.setItem(item.getItem(), getX() + x + paneOffsetX, getY() + y + paneOffsetY);
    }
  }

  @Override
  public boolean click(Gui Gui, InventoryClickEvent event, int slot, int paneOffsetX, int paneOffsetY, int maxLength, int maxHeight) {
    ItemStack itemStack = event.getCurrentItem();

    if (itemStack == null) {
      return false;
    }

    GuiItem item = findMatchingItem(Arrays.asList(items), itemStack);

    if (item == null) {
      return false;
    }

    item.callAction(event);

    return true;
  }

  public void addItem(GuiItem item) {
    items[ThreadLocalRandom.current().nextInt(items.length)] = item;
  }

  @Override
  public Collection<GuiItem> getItems() {
    return Arrays.asList(items);
  }

  @Override
  public Collection<Pane> getPanes() {
    return new ArrayList<>();
  }

  @Override
  public void clear() {}
}

XML

Alright to make your pane at least somewhat possible to work with XML we'll first have to register it. You can register your pane like so:

Gui.registerPane("randompane", (instance, element) -> {});

The name specified is the name you'll be using inside your XML file, in this case randompane. Now you notice that we have to load this thing. I highly recommend you to create a separate method in your class for this, since it gets quite messy inside a lambda quite fast.

static RandomPane load(Object instance, Element element) {

}

So first we'll wrap this thing in a try/catch statement with a NumberFormatException (we're not gonna check the values for correct number parsing, although you may do this in production code).

try {

} catch (NumberFormatException exception) {
  exception.printStackTrace();
  return null;
}

So inside the try block we'll first have to create our RandomPane, based on the attributes specified in the XML file. Getting an attribute is really simple, just call getAttribute.

RandomPane pane = new RandomPane(
  Integer.parseInt(element.getAttribute("length")),
  Integer.parseInt(element.getAttribute("height"))
);

Now there is a simple method created which loads a bunch of default values for you, so we're gonna call that now.

Pane.load(pane, instance, element);

If you have implemented either Orientable, Flippable or Rotatable, you can now call Orientable.load, Flippable.load or Rotatable.load to directly load the properties that belong to these.

Alright, most stuff is done, however in case we had a populate attribute, we'll need to cancel everything that we're gonna do after, so return the pane if that's the case.

if (element.hasAttribute("populate")) {
  return pane;
}

Now we need to load all items inside this pane, so we're gonna take a list of children and loop over them.

NodeList childNodes = element.getChildNodes();

for (int i = 0; i < childNodes.getLength(); i++) {
  Node item = childNodes.item(i);
}

Now that we have that we need to ensure that our node is indeed an element (nodes and elements slightly differ, but we only care about the elements).

if (item.getNodeType() != Node.ELEMENT_NODE) {
  continue;
}

Element child = (Element) item;

Now that we have the element we can add it to our random pane. We can load the item with another internal method, so we don't have to worry about that.

randomPane.addItem(Pane.loadItem(instance, child));

And now, outside of the for loop, you can return the pane and you're done.

return pane;
Clone this wiki locally