Skip to content

Menu DSL

Gabriel Souza edited this page Jul 5, 2020 · 13 revisions

Without a doubt, the Menu DSL is one of the best things that KotlinBukkitAPI can help you. In this part of the Wiki we will look at it.

Basics

fun Plugin.menu(
        displayName: String,
        lines: Int,
        cancelOnClick: Boolean = true,
        block: MenuDSL.() -> Unit
): MenuDSL
  • displayName is the name in the Menu, this can be changed when the Menu renders to the Player.
  • cancelOnClick if should cancel on player click on an item (commonly this will be always true).
val myMenu = menu("Servers", 4, true) {
  ...
}
fun MenuDSL.slot(
        line: Int,
        slot: Int,
        item: ItemStack?,
        block: SlotDSL.() -> Unit = {}
): SlotDSL 
  • line is the inventory line of the slot
  • slot is the slot of the line starting in 1 to 9
  • item the item to be displayed at this slot
val myMenu = menu("Servers", 4, true) {
  slot(2, 3, item(Material.TNT).displayName("§cFactions")) {
    ...
  }
  slot(2, 5, item(Material.GRASS).displayName("§aCreative")) {
    ...
  }
  slot(2, 7, item(Material.IRON_PICKAXE).displayName("§cPrision")) {
    ...
  }

  val coinsItem = item(Material.EMERALD) {
    displayName = "§2Network coins"
    lore = listOf(
      "  ",
      " §2Global coins: §b{global_coins}"
      " §2Prision coins: §b{prision_coins}
    )
  }
  slot(4, 5, coinsItem) {
    ...
  }
}

RESULT

Listen players clicks

Let's ignore the coinsItem for a second just for the code be small in the examples.

typealias MenuPlayerSlotInteractEvent = MenuPlayerSlotInteract.() -> Unit

// class SlotDSL:
fun onClick(click: MenuPlayerSlotInteractEvent)

class MenuPlayerSlotInteract(
        menu: Menu<*>,
        override val slotPos: Int,
        override val slot: Slot,
        player: Player,
        inventory: Inventory,
        canceled: Boolean,
        val click: ClickType,
        val action: InventoryAction,
        val clicked: ItemStack?,
        val cursor: ItemStack?,
        val hotbarKey: Int
) : MenuPlayerInteract
  • slotPos is the current slot post in the Menu
  • player that is clicking in the Item
  • var canceled if should cancel the Player pick the item (if you put in the Menu build cancelOnClick you don't need to change this value)
  • clicked the current ItemStack that is in the Inventory.
val myMenu = menu("Servers", 4, true) {
  slot(2, 3, item(Material.TNT).displayName("§cFactions")) {
    onClick {
      player.msg("§eTeleporting to Factions server.")
      player.bungeecord.send("factions")
      close() // close the Menu
    }
  }
  slot(2, 5, item(Material.GRASS).displayName("§aCreative")) {
    onClick {
      player.msg("§eTeleporting to Creative server.")
      player.bungeecord.send("creative")
      close() // close the Menu
    }
  }
  slot(2, 7, item(Material.IRON_PICKAXE).displayName("§cPrision")) {
    onClick {
      player.msg("§eTeleporting to Prision server.")
      player.bungeecord.send("prision")
      close() // close the Menu
    }
  }
}

RESULT GIF HERE

NOTE that is not teleporting to Other server because I'm not using BungeeCord. If you want to know more about KotlinBukkitAPI BungeeCord extesions, check the documentation.

Change item to a Player on open Menu

Note that in your coins menu item, he has variables that is not Global, it is values for the Player that is opening the menu too.

val coinsItem = item(Material.EMERALD) {
    displayName = "§2Network coins"
    lore = listOf(
            "  ",
            " §2Global coins: §b{global_coins}"
            " §2Prision coins: §b{prision_coins}
    )
}
slot(4, 5, coinsItem) {
    ...
}
  • {global_coins}, {prision_coins}

We can use onRender to listen when the Slot is rendering to the Player (on open menu) and update the Item.

typealias MenuPlayerSlotRenderEvent = MenuPlayerSlotRender.() -> Unit

fun onRender(render: MenuPlayerSlotRenderEvent)

class MenuPlayerSlotRender(
        override val menu: Menu<*>,
        override val slotPos: Int,
        override val slot: Slot,
        override val player: Player,
        override val inventory: Inventory
) : MenuPlayerInventorySlot

interface MenuPlayerInventorySlot {
  var showingItem: ItemStack?
  
  ...
}
  • slotPos is the current slot post in the Menu
  • player that is clicking in the Item
  • showingItem is the current ItemStack that is in the Player inventory.
val coinsItem = item(Material.EMERALD) {
  displayName = "§2Network coins"
  lore = listOf(
          "  ",
          " §2Global coins: §b{global_coins}"
          " §2Prision coins: §b{prision_coins}"
  )
}
slot(4, 5, coinsItem) {
  onRender {
    showingItem?.meta<ItemMeta> {
      lore = lore.map {
        it.replace("{global_coins}", CoinsManager.getGlobalCoinsFormated(player))
        it.replace("{prision_coins}", CoinsManager.getPresionCoinsFormated(player))
      }
    }
  }
}

RESULT

** Note ** that slot(...) support null items, this means that if you want to set a item only in onRender, this is possible!

slot(4, 5, null) {
  onRender {
    showingItem = item(Material.DIAMOND)
  }
}

Auto update the Menu

Let's change a little bit your Servers "buttons" to add the players online count of the server. The goal is that even with Menu open, the online player count keep updating with the current value of the server.

Again, without the coinsItem to make the sample small.

val myMenu = menu("Servers", 4, true) {
  val playersOnlineLore = listOf(" ", " §2Players online: {players_online} ")
  slot(2, 3, item(Material.TNT).displayName("§cFactions")) {
    onClick {
      player.msg("§eTeleporting to Factions server.")
      player.bungeecord.send("factions")
      close() // close the Menu
    }
    onRender {
      showingItem?.meta<ItemMeta> {
        lore = playersOnlineLore.map {
          it.replace("{players_online}", ServerManager.getOnlinePlayers("factions"))
        }
      }
    }
  }
  slot(2, 5, item(Material.GRASS).displayName("§aCreative")) {
    onClick {
      player.msg("§eTeleporting to Creative server.")
      player.bungeecord.send("creative")
      close() // close the Menu
    }
    onRender {
      showingItem?.meta<ItemMeta> {
        lore = playersOnlineLore.map {
          it.replace("{players_online}", ServerManager.getOnlinePlayers("factions"))
        }
      }
    }
  }
  slot(2, 7, item(Material.IRON_PICKAXE).displayName("§cPrision")) {
    onClick {
      player.msg("§eTeleporting to Prision server.")
      player.bungeecord.send("prision")
      close() // close the Menu
    }
    onRender {
      showingItem?.meta<ItemMeta> {
        lore = playersOnlineLore.map {
          it.replace("{players_online}", ServerManager.getOnlinePlayers("factions"))
        }
      }
    }
  }
}

This is getting big, later we will fix it, but now, lets make your Slot auto update.

To achieve that goal, we will use a property of the Menu called updateDelay that define the update time of the menu in ticks.

And we will use onUpdate block from SlotDSL.

typealias MenuPlayerSlotUpdateEvent = MenuPlayerSlotUpdate.() -> Unit

// SlotDSL function
fun onUpdate(update: MenuPlayerSlotUpdateEvent)

class MenuPlayerSlotUpdate(
        override val menu: Menu<*>,
        override val slotPos: Int,
        override val slot: Slot,
        override val player: Player,
        override val inventory: Inventory
) : MenuPlayerInventorySlot
  • slotPos is the current slot post in the Menu
  • player that is clicking in the Item

We have the same structure from MenuPlayerSlotRender.

interface MenuPlayerInventorySlot {
  var showingItem: ItemStack?
  
  fun updateSlotToPlayer()

  fun updateSlot()
}
  • showingItem is the current ItemStack that is in the Player inventory.
  • fun updateSlotToPlayer(): updates the slot, calling onUpdate block to the current player viewing Menu.
  • fun updateSlot(): updates the slot, calling onUpdate block to all players viewing the Menu.
val myMenu = menu("Servers", 4, true) {
  updateDelay = 20 // 1 second

  val playersOnlineLore = listOf(" ", " §2Players online: {players_online} ")
  slot(2, 3, item(Material.TNT).displayName("§cFactions")) {
    onClick {
      player.msg("§eTeleporting to Factions server.")
      player.bungeecord.send("factions")
      close() // close the Menu
    }
    onRender {
      showingItem?.meta<ItemMeta> {
        lore = playersOnlineLore.map {
          it.replace("{players_online}", ServerManager.getOnlinePlayers("factions"))
        }
      }
    }
    onUpdate {
      showingItem?.meta<ItemMeta> {
        lore = playersOnlineLore.map {
          it.replace("{players_online}", ServerManager.getOnlinePlayers("factions"))
        }
      }
    }
  }
  slot(2, 5, item(Material.GRASS).displayName("§aCreative")) {
    onClick {
      player.msg("§eTeleporting to Creative server.")
      player.bungeecord.send("creative")
      close() // close the Menu
    }
    onRender {
      showingItem?.meta<ItemMeta> {
        lore = playersOnlineLore.map {
          it.replace("{players_online}", ServerManager.getOnlinePlayers("creative"))
        }
      }
    }
    onUpdate {
      showingItem?.meta<ItemMeta> {
        lore = playersOnlineLore.map {
          it.replace("{players_online}", ServerManager.getOnlinePlayers("creative"))
        }
      }
    }
  }
  slot(2, 7, item(Material.IRON_PICKAXE).displayName("§cPrision")) {
    onClick {
      player.msg("§eTeleporting to Prision server.")
      player.bungeecord.send("prision")
      close() // close the Menu
    }
    onRender {
      showingItem?.meta<ItemMeta> {
        lore = playersOnlineLore.map {
          it.replace("{players_online}", ServerManager.getOnlinePlayers("prision"))
        }
      }
    }
    onUpdate {
      showingItem?.meta<ItemMeta> {
        lore = playersOnlineLore.map {
          it.replace("{players_online}", ServerManager.getOnlinePlayers("prision"))
        }
      }
    }
  }
}

Uff... Is getting bigger and... repetitive...

The onUpdate and onRender is the same, we can use Kotlin extension function to solve this problem because the MenuPlayerSlotRender and MenuPlayerSlotUpdate implements MenuPlayerInventorySlot.

val myMenu = menu("Servers", 4, true) {
  updateDelay = 20 // 1 second

  val playersOnlineLore = listOf(" ", " §2Players online: {players_online} ")

  fun MenuPlayerInventorySlot.updateCurrentPlayersCount(
      server: String
  ) {
    showingItem?.meta<ItemMeta> {
      lore = playersOnlineLore.map {
        it.replace("{players_online}", ServerManager.getOnlinePlayers(server))
      }
    }
  }

  slot(2, 3, item(Material.TNT).displayName("§cFactions")) {
    onClick {
      player.msg("§eTeleporting to Factions server.")
      player.bungeecord.send("factions")
      close() // close the Menu
    }
    onRender { updateCurrentPlayersCount("factions") }
    onUpdate { updateCurrentPlayersCount("factions") }
  }
  slot(2, 5, item(Material.GRASS).displayName("§aCreative")) {
    onClick {
      player.msg("§eTeleporting to Creative server.")
      player.bungeecord.send("creative")
      close() // close the Menu
    }
    onRender { updateCurrentPlayersCount("creative") }
    onUpdate { updateCurrentPlayersCount("creative") }
  }
  slot(2, 7, item(Material.IRON_PICKAXE).displayName("§cPrision")) {
    onClick {
      player.msg("§eTeleporting to Prision server.")
      player.bungeecord.send("presion")
      close() // close the Menu
    }
    onRender { updateCurrentPlayersCount("prision") }
    onUpdate { updateCurrentPlayersCount("prision") }
  }
}

Much better... But we can do that with onClick to, is basic the same...

val myMenu = menu("Servers", 4, true) {
  updateDelay = 20 // 1 second

  val playersOnlineLore = listOf(" ", " §2Players online: {players_online} ")

  fun MenuPlayerInventorySlot.updateCurrentPlayersCount(
      server: String
  ) {
    showingItem?.meta<ItemMeta> {
      lore = playersOnlineLore.map {
        it.replace("{players_online}", ServerManager.getOnlinePlayers(server))
      }
    }
  }

  fun MenuPlayerSlotInteract.teleportPlayerToServerAndNotify(
      bungeeServer: String, displayName: String
  ) {
    player.msg("§eTeleporting to $displayName server.")
    player.bungeecord.send(bungeeServer)
    close() // close the Menu
  }

  slot(2, 3, item(Material.TNT).displayName("§cFactions")) {
    onClick { teleportPlayerToServerAndNotify("faction", "Factions") }
    onRender { updateCurrentPlayersCount("factions") }
    onUpdate { updateCurrentPlayersCount("factions") }
  }
  slot(2, 5, item(Material.GRASS).displayName("§aCreative")) {
    onClick { teleportPlayerToServerAndNotify("creative", "Creative") }
    onRender { updateCurrentPlayersCount("creative") }
    onUpdate { updateCurrentPlayersCount("creative") }
  }
  slot(2, 7, item(Material.IRON_PICKAXE).displayName("§cPrision")) {
    onClick { teleportPlayerToServerAndNotify("prision", "Prision") }
    onRender { updateCurrentPlayersCount("prision") }
    onUpdate { updateCurrentPlayersCount("prision") }
  }

  val coinsItem = item(Material.EMERALD) {
    displayName = "§2Network coins"
    lore = listOf(
        "  ",
        " §2Global coins: §b{global_coins}"
        " §2Prision coins: §b{prision_coins}"
    )
  }
  slot(4, 5, coinsItem) {
    onRender {
      showingItem?.meta<ItemMeta> {
        lore = lore.map {
          it.replace("{global_coins}", CoinsManager.getGlobalCoinsFormated(player))
          it.replace("{prision_coins}", CoinsManager.getPresionCoinsFormated(player))
        }
      }
    }
  }
}

RESULT

This is the basics of the Menu DSL, to keep learning, check out the Menu DSL: Advanced documentation.

If you want to navigate into the code, you can check this two folder that contains all Menu structure.

Clone this wiki locally