diff --git a/.idea/modules.xml b/.idea/modules.xml
index b3f03b10..bb04ac9d 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -16,7 +16,6 @@
-
@@ -31,31 +30,23 @@
-
-
-
-
-
-
-
-
diff --git a/gradle.properties b/gradle.properties
index fc2f1726..aab0d4aa 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,5 +7,5 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
javaVersion=21
mcVersion=1.21.4
group=dev.slne.surf
-version=1.21.4-2.15.1-SNAPSHOT
+version=1.21.4-2.16.0-SNAPSHOT
relocationPrefix=dev.slne.surf.surfapi.libs
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 35520229..1bfffc84 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -50,7 +50,6 @@ reflection-remapper = "0.1.1"
brigadier = "1.0.18"
configurate = "4.1.2"
more-persistent-data-types = "2.4.0"
-inventoryframework = "0.10.19"
flogger = "0.8"
aide-reflection = "1.3"
auto-service = "1.1.1"
@@ -136,7 +135,6 @@ configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref
configurate-jackson = { module = "org.spongepowered:configurate-jackson", version.ref = "configurate" }
configurate-kotlin = { module = "org.spongepowered:configurate-extra-kotlin", version.ref = "configurate" }
more-persistent-data-types = { module = "com.jeff_media:MorePersistentDataTypes", version.ref = "more-persistent-data-types" }
-inventoryframework = { module = "com.github.stefvanschie.inventoryframework:IF", version.ref = "inventoryframework" }
flogger = { module = "com.google.flogger:flogger", version.ref = "flogger" }
flogger-slf4j-backend = { module = "com.google.flogger:flogger-slf4j-backend", version.ref = "flogger" }
aide-reflection = { module = "tech.hiddenproject:aide-reflection", version.ref = "aide-reflection" }
diff --git a/surf-api-bukkit/surf-api-bukkit-api/build.gradle.kts b/surf-api-bukkit/surf-api-bukkit-api/build.gradle.kts
index c1e95fac..d7df1a4e 100644
--- a/surf-api-bukkit/surf-api-bukkit-api/build.gradle.kts
+++ b/surf-api-bukkit/surf-api-bukkit-api/build.gradle.kts
@@ -11,7 +11,6 @@ dependencies {
compileOnlyApi(libs.commandapi.bukkit)
compileOnlyApi(libs.reflection.remapper)
compileOnlyApi(libs.more.persistent.data.types)
- compileOnlyApi(libs.inventoryframework)
api(libs.commandapi.bukkit.kotlin)
compileOnlyApi(libs.mccoroutine.folia.api)
@@ -22,3 +21,10 @@ description = "surf-api-bukkit-api"
configurations.all {
exclude(group = "org.spigotmc", module = "spigot-api")
}
+
+kotlin {
+ compilerOptions {
+ optIn.add("dev.slne.surf.surfapi.core.api.util.InternalSurfApi")
+ optIn.add("kotlin.contracts.ExperimentalContracts")
+ }
+}
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/InventoryBridge.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/InventoryBridge.kt
new file mode 100644
index 00000000..37ee2da7
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/InventoryBridge.kt
@@ -0,0 +1,42 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.Gui
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.types.ChestGui
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.UpdatableGuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.OutlinePane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.PaginatedPane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.StaticPane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Mask
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Pattern
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Slot
+import dev.slne.surf.surfapi.core.api.util.InternalSurfApi
+import dev.slne.surf.surfapi.core.api.util.requiredService
+import org.bukkit.NamespacedKey
+import org.bukkit.inventory.Inventory
+import java.util.*
+
+@InternalSurfApi
+interface InventoryBridge {
+
+ fun getGuiByInventory(inventory: Inventory): Gui?
+ fun createGuiItem(key: NamespacedKey?, uuid: UUID?): GuiItem
+ fun createUpdatableGuiItem(
+ key: NamespacedKey?,
+ uuid: UUID?
+ ): UpdatableGuiItem
+
+ fun createMask(vararg mask: String): Mask
+ fun createPattern(vararg pattern: String): Pattern
+
+ fun createChestGui(size: ChestGui.ChestGuiSize, parent: Gui? = null): ChestGui
+
+
+ fun createOutlinePane(slot: Slot, length: Int, height: Int, uuid: UUID?): OutlinePane
+ fun createPaginatedPane(slot: Slot, length: Int, height: Int, uuid: UUID?): PaginatedPane
+ fun createStaticPane(slot: Slot, length: Int, height: Int, uuid: UUID?): StaticPane
+
+ companion object {
+ val instance = requiredService()
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SinglePlayerGui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SinglePlayerGui.kt
deleted file mode 100644
index 7f3cc95c..00000000
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SinglePlayerGui.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package dev.slne.surf.surfapi.bukkit.api.inventory
-
-import org.bukkit.entity.Player
-
-interface SinglePlayerGui : SurfGui {
- val player: Player
-
- fun open() = gui.show(player)
-}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SurfGui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SurfGui.kt
deleted file mode 100644
index 0983099b..00000000
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/SurfGui.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package dev.slne.surf.surfapi.bukkit.api.inventory
-
-import com.github.stefvanschie.inventoryframework.gui.type.util.NamedGui
-import com.github.stefvanschie.inventoryframework.pane.StaticPane
-import com.github.stefvanschie.inventoryframework.pane.util.Slot
-import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.PaneMarker
-import dev.slne.surf.surfapi.bukkit.api.inventory.item.SurfGuiItem
-import org.bukkit.entity.HumanEntity
-import org.bukkit.event.inventory.InventoryCloseEvent
-import org.bukkit.inventory.ItemStack
-import org.bukkit.plugin.java.JavaPlugin
-
-interface SurfGui {
- val parent: SurfGui?
- val gui: NamedGui
-
- fun HumanEntity.backToParent() {
- server.scheduler.runTaskLater(JavaPlugin.getProvidingPlugin(SurfGui::class.java), Runnable {
- if (parent != null) {
- val gui = parent!!.gui
- gui.show(this)
- gui.update()
- } else {
- closeInventory(InventoryCloseEvent.Reason.PLUGIN)
- }
- }, 1L)
- }
-
- fun walkParents(): List = generateSequence(this) { it.parent }.toList()
-
- fun StaticPane.item(
- slot: Slot,
- item: ItemStack? = null,
- init: (@PaneMarker SurfGuiItem).() -> Unit,
- ) {
- val guiItem = SurfGuiItem(item)
- guiItem.init()
-
- if (!guiItem.condition()) {
- return
- }
-
- if (this@SurfGui is SinglePlayerGui) {
- if (guiItem.itemPermission?.let { player.hasPermission(it) } == false) {
- return
- }
- }
-
- addItem(guiItem, slot)
- }
-}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/gui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/gui.kt
deleted file mode 100644
index bb8cb947..00000000
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/gui.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-@file:OptIn(ExperimentalContracts::class)
-
-package dev.slne.surf.surfapi.bukkit.api.inventory.dsl
-
-import com.github.stefvanschie.inventoryframework.gui.type.util.MergedGui
-import com.github.stefvanschie.inventoryframework.pane.StaticPane
-import com.github.stefvanschie.inventoryframework.pane.util.Slot
-import dev.slne.surf.surfapi.bukkit.api.inventory.types.SurfChestGui
-import dev.slne.surf.surfapi.bukkit.api.inventory.types.SurfChestSinglePlayerGui
-import net.kyori.adventure.text.Component
-import org.bukkit.entity.Player
-import org.jetbrains.annotations.Range
-import kotlin.contracts.ExperimentalContracts
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
-
-@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
-@DslMarker
-annotation class MenuMarker
-
-fun MergedGui.staticPane(
- slot: Slot,
- height: @Range(from = 1, to = 6) Int,
- length: @Range(from = 1, to = 9) Int = 9,
- init: (@PaneMarker StaticPane).() -> Unit,
-) {
- contract {
- callsInPlace(init, InvocationKind.EXACTLY_ONCE)
- }
-
- val pane = StaticPane(slot, length, height)
- pane.init()
- addPane(pane)
-}
-
-fun menu(
- title: Component,
- rows: @Range(from = 2, to = 6) Int = 6,
- init: @MenuMarker SurfChestGui.() -> Unit,
-): SurfChestGui {
- contract {
- callsInPlace(init, InvocationKind.EXACTLY_ONCE)
- }
-
- val menu = SurfChestGui(title, rows)
- menu.init()
- return menu
-}
-
-fun playerMenu(
- title: Component,
- player: Player,
- rows: @Range(from = 2, to = 6) Int = 6,
- init: @MenuMarker SurfChestSinglePlayerGui.() -> Unit,
-): SurfChestSinglePlayerGui {
- contract {
- callsInPlace(init, InvocationKind.EXACTLY_ONCE)
- }
-
- val menu = SurfChestSinglePlayerGui(title, player, rows)
- menu.init()
- return menu
-}
-
-
-fun SurfChestGui.childMenu(
- title: Component,
- rows: @Range(from = 2, to = 6) Int,
- init: @MenuMarker SurfChestGui.() -> Unit,
-): SurfChestGui {
- contract {
- callsInPlace(init, InvocationKind.EXACTLY_ONCE)
- }
-
- val menu = SurfChestGui(title, rows, this)
- menu.init()
- return menu
-}
-
-fun SurfChestSinglePlayerGui.childPlayerMenu(
- title: Component,
- rows: @Range(from = 2, to = 6) Int,
- init: @MenuMarker SurfChestSinglePlayerGui.() -> Unit,
-): SurfChestSinglePlayerGui {
- contract {
- callsInPlace(init, InvocationKind.EXACTLY_ONCE)
- }
-
- val menu = SurfChestSinglePlayerGui(title, player, rows, this)
- menu.init()
- return menu
-}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/markers.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/markers.kt
index eee9a825..bb34fbda 100644
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/markers.kt
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/markers.kt
@@ -3,3 +3,11 @@ package dev.slne.surf.surfapi.bukkit.api.inventory.dsl
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
@DslMarker
annotation class PaneMarker
+
+@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
+@DslMarker
+annotation class GuiDsl
+
+@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
+@DslMarker
+annotation class GuiItemDsl
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/panes.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/panes.kt
deleted file mode 100644
index 34635aaf..00000000
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/panes.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-@file:OptIn(ExperimentalContracts::class)
-
-package dev.slne.surf.surfapi.bukkit.api.inventory.dsl
-
-import com.github.stefvanschie.inventoryframework.gui.GuiItem
-import com.github.stefvanschie.inventoryframework.pane.OutlinePane
-import com.github.stefvanschie.inventoryframework.pane.Pane
-import com.github.stefvanschie.inventoryframework.pane.StaticPane
-import com.github.stefvanschie.inventoryframework.pane.util.Slot
-import dev.slne.surf.surfapi.bukkit.api.inventory.item.guiItem
-import dev.slne.surf.surfapi.bukkit.api.inventory.pane.SubmitItemPane
-import dev.slne.surf.surfapi.bukkit.api.inventory.types.SurfChestGui
-import org.bukkit.Material
-import org.bukkit.inventory.ItemStack
-import org.jetbrains.annotations.Range
-import kotlin.contracts.ExperimentalContracts
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
-
-@PaneMarker
-class StaticPaneScope(slot: Slot, length: Int, height: Int) : StaticPane(slot, length, height)
-
-fun SurfChestGui.drawOutline(
- slot: Slot,
- height: @Range(from = 1, to = 6) Int,
- length: @Range(from = 1, to = 9) Int = 9,
- init: @PaneMarker OutlinePane.() -> Unit
-): OutlinePane {
- contract {
- callsInPlace(init, InvocationKind.EXACTLY_ONCE)
- }
-
- val pane = OutlinePane(slot, length, height, Pane.Priority.LOWEST)
- pane.init()
- addPane(pane)
-
- return pane
-}
-
-fun SurfChestGui.drawOutline(
- slot: Slot,
- height: @Range(from = 1, to = 6) Int,
- length: @Range(from = 1, to = 9) Int = 9,
- item: GuiItem = guiItem(Material.GRAY_STAINED_GLASS_PANE) { isCancelled = true }
-) = drawOutline(slot, height, length) {
- addItem(item)
- setRepeat(true)
-}
-
-@OptIn(ExperimentalContracts::class)
-fun SurfChestGui.drawOutlineRow(
- row: @Range(from = 0, to = 5) Int,
- length: @Range(from = 1, to = 9) Int = 9,
- init: @PaneMarker OutlinePane.() -> Unit
-): OutlinePane {
- contract {
- callsInPlace(init, InvocationKind.EXACTLY_ONCE)
- }
- return drawOutline(slot(0, row), 1, length, init)
-}
-
-fun SurfChestGui.drawOutlineRow(
- row: @Range(from = 0, to = 5) Int,
- length: @Range(from = 1, to = 9) Int = 9,
- item: GuiItem = guiItem(Material.GRAY_STAINED_GLASS_PANE) { isCancelled = true }
-) = drawOutlineRow(row, length) {
- addItem(item)
- setRepeat(true)
-}
-
-fun SurfChestGui.makeStaticPane(
- slot: Slot,
- height: @Range(from = 1, to = 6) Int,
- length: @Range(from = 1, to = 9) Int,
- init: StaticPane.() -> Unit
-): StaticPane {
- contract {
- callsInPlace(init, InvocationKind.EXACTLY_ONCE)
- }
-
- val pane = StaticPane(slot, length, height)
- pane.init()
- addPane(pane)
-
- return pane
-}
-
-fun SurfChestGui.makeSubmitItemPane(
- slot: Slot,
- length: @Range(from = 1, to = 6) Int,
- height: @Range(from = 1, to = 6) Int,
- filter: List,
- init: SubmitItemPane.() -> Unit = {}
-): SubmitItemPane {
- contract {
- callsInPlace(init, InvocationKind.EXACTLY_ONCE)
- }
-
- val pane = SubmitItemPane(slot, length, height, filter)
- pane.init()
- addPane(pane)
-
- return pane
-}
-
-fun SurfChestGui.makeSubmitItemPane(
- slot: Slot,
- length: @Range(from = 1, to = 6) Int,
- height: @Range(from = 1, to = 6) Int,
- filter: (ItemStack) -> Boolean,
- init: SubmitItemPane.() -> Unit = {},
-): SubmitItemPane {
- contract {
- callsInPlace(init, InvocationKind.EXACTLY_ONCE)
- }
-
- val pane = SubmitItemPane(slot, length, height, filter)
- pane.init()
- addPane(pane)
-
- return pane
-}
-
-fun SurfChestGui.addItem(
- slot: Slot,
- item: GuiItem
-) = addPane(StaticPane(slot, 1, 1).apply { addItem(item, 0, 0) })
-
-fun SurfChestGui.addItems(
- vararg items: Pair
-) = items.forEach { (slot, item) -> addItem(slot, item) }
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/patterns.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/patterns.kt
deleted file mode 100644
index 60feb449..00000000
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/patterns.kt
+++ /dev/null
@@ -1,2 +0,0 @@
-package dev.slne.surf.surfapi.bukkit.api.inventory.dsl
-
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/slot.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/slot.kt
deleted file mode 100644
index 1624fc16..00000000
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/dsl/slot.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package dev.slne.surf.surfapi.bukkit.api.inventory.dsl
-
-import com.github.stefvanschie.inventoryframework.pane.util.Slot
-import org.jetbrains.annotations.Range
-
-fun slot(x: @Range(from = 0, to = 8) Int, y: @Range(from = 0, to = 5) Int) = Slot.fromXY(x, y)
-fun slot(index: Int) = Slot.fromIndex(index)
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/Gui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/Gui.kt
new file mode 100644
index 00000000..374a4053
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/Gui.kt
@@ -0,0 +1,58 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.gui
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.InventoryBridge
+import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.GuiDsl
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.*
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.UpdatableGuiItem
+import it.unimi.dsi.fastutil.objects.ObjectSet
+import org.bukkit.entity.Player
+import org.bukkit.inventory.Inventory
+import org.jetbrains.annotations.Unmodifiable
+
+@GuiDsl
+interface Gui : Cloneable {
+ val backingInventory: Inventory
+ val viewers: @Unmodifiable ObjectSet
+ val parent: Gui?
+
+ // region Handlers
+ fun onTopClick(handler: ClickHandler)
+
+ fun onTopClick(handler: ClickHandlerDsl)
+ fun onBottomClick(handler: ClickHandler)
+
+ fun onBottomClick(handler: ClickHandlerDsl)
+ fun onGlobalClick(handler: ClickHandler)
+
+ fun onGlobalClick(handler: ClickHandlerDsl)
+ fun onOutsideClick(handler: ClickHandler)
+
+ fun onOutsideClick(handler: ClickHandlerDsl)
+ fun onTopDrag(handler: DragHandler)
+
+ fun onTopDrag(handler: DragHandlerDsl)
+ fun onBottomDrag(handler: DragHandler)
+
+ fun onBottomDrag(handler: DragHandlerDsl)
+ fun onGlobalDrag(handler: DragHandler)
+
+ fun onGlobalDrag(handler: DragHandlerDsl)
+ fun onClose(handler: CloseHandler)
+ fun onClose(handler: CloseHandlerDsl)
+ // endregion
+
+ fun navigateToParentOnClose(enabled: Boolean = true)
+ fun navigateToParent(player: Player): Boolean
+ fun walkParents(): Sequence
+
+ fun show(player: Player)
+ fun update()
+ fun updateItem(item: UpdatableGuiItem)
+
+ public override fun clone(): Gui
+
+ companion object {
+ fun getGui(inventory: Inventory): Gui? =
+ InventoryBridge.instance.getGuiByInventory(inventory)
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/MergedGui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/MergedGui.kt
new file mode 100644
index 00000000..1188c80b
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/MergedGui.kt
@@ -0,0 +1,63 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.gui
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.InventoryBridge
+import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.GuiDsl
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.Pane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.OutlinePane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.PaginatedPane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.StaticPane
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.InventoryComponent
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Slot
+import it.unimi.dsi.fastutil.objects.ObjectList
+import org.jetbrains.annotations.Unmodifiable
+import java.util.*
+
+@GuiDsl
+interface MergedGui {
+ val inventoryComponent: InventoryComponent
+ val panes: @Unmodifiable ObjectList
+ val items: @Unmodifiable ObjectList
+
+
+ fun addPane(pane: Pane)
+
+ fun outlinePane(
+ slot: Slot,
+ length: Int,
+ height: Int,
+ uuid: UUID? = null,
+ init: OutlinePane.() -> Unit,
+ ): OutlinePane {
+ val pane = InventoryBridge.instance.createOutlinePane(slot, length, height, uuid)
+ pane.init()
+ addPane(pane)
+ return pane
+ }
+
+ fun paginatedPane(
+ slot: Slot,
+ length: Int,
+ height: Int,
+ uuid: UUID? = null,
+ init: PaginatedPane.() -> Unit,
+ ): PaginatedPane {
+ val pane = InventoryBridge.instance.createPaginatedPane(slot, length, height, uuid)
+ pane.init()
+ addPane(pane)
+ return pane
+ }
+
+ fun staticPane(
+ slot: Slot,
+ length: Int,
+ height: Int,
+ uuid: UUID? = null,
+ init: StaticPane.() -> Unit,
+ ): StaticPane {
+ val pane = InventoryBridge.instance.createStaticPane(slot, length, height, uuid)
+ pane.init()
+ addPane(pane)
+ return pane
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/NamedGui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/NamedGui.kt
new file mode 100644
index 00000000..1e26228d
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/NamedGui.kt
@@ -0,0 +1,15 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.gui
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.GuiDsl
+import dev.slne.surf.surfapi.core.api.messages.builder.SurfComponentBuilder
+import net.kyori.adventure.text.Component
+
+@GuiDsl
+interface NamedGui : Gui {
+ val title: Component
+
+ fun title(title: Component)
+ fun title(builder: SurfComponentBuilder.() -> Unit)
+
+ override fun clone(): NamedGui
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/ClickHandlers.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/ClickHandlers.kt
new file mode 100644
index 00000000..a8fefd79
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/ClickHandlers.kt
@@ -0,0 +1,20 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers
+
+import dev.slne.surf.surfapi.core.api.util.InternalSurfApi
+import org.bukkit.entity.Player
+import org.bukkit.event.inventory.InventoryClickEvent
+
+typealias ClickHandlerDsl = ClickHandlerScope.() -> Unit
+
+interface ClickHandler : GuiHandler
+
+@JvmInline
+value class ClickHandlerScope @InternalSurfApi constructor(val event: InventoryClickEvent) {
+ val player
+ get() = event.whoClicked as? Player ?: error("Click event is not triggered by a player")
+
+ fun cancel() {
+ event.isCancelled = true
+ }
+}
+
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/CloseHandlers.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/CloseHandlers.kt
new file mode 100644
index 00000000..720155e4
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/CloseHandlers.kt
@@ -0,0 +1,16 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers
+
+import dev.slne.surf.surfapi.core.api.util.InternalSurfApi
+import org.bukkit.entity.Player
+import org.bukkit.event.inventory.InventoryCloseEvent
+
+typealias CloseHandlerDsl = CloseHandlerScope.() -> Unit
+
+interface CloseHandler: GuiHandler
+
+@JvmInline
+value class CloseHandlerScope @InternalSurfApi constructor(val event: InventoryCloseEvent) {
+ val player
+ get() = event.player as? Player ?: error("Close event is not triggered by a player")
+}
+
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/DragHandlers.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/DragHandlers.kt
new file mode 100644
index 00000000..3e531ccc
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/DragHandlers.kt
@@ -0,0 +1,19 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers
+
+import dev.slne.surf.surfapi.core.api.util.InternalSurfApi
+import org.bukkit.entity.Player
+import org.bukkit.event.inventory.InventoryDragEvent
+
+typealias DragHandlerDsl = DragHandlerScope.() -> Unit
+
+interface DragHandler : GuiHandler
+
+@JvmInline
+value class DragHandlerScope @InternalSurfApi constructor(val event: InventoryDragEvent) {
+ val player
+ get() = event.whoClicked as? Player ?: error("Drag event is not triggered by a player")
+
+ fun cancel() {
+ event.isCancelled = true
+ }
+}
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/GuiHandler.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/GuiHandler.kt
new file mode 100644
index 00000000..fdc822cf
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/handlers/GuiHandler.kt
@@ -0,0 +1,9 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers
+
+import org.bukkit.event.Event
+import org.jetbrains.annotations.ApiStatus.OverrideOnly
+
+interface GuiHandler {
+ @OverrideOnly
+ fun handle(event: E)
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/types/ChestGui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/types/ChestGui.kt
new file mode 100644
index 00000000..8988c52a
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/gui/types/ChestGui.kt
@@ -0,0 +1,56 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.gui.types
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.InventoryBridge
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.Gui
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.MergedGui
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.NamedGui
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
+interface ChestGui : NamedGui, MergedGui {
+ val size: ChestGuiSize
+ fun size(size: ChestGuiSize)
+
+ override fun clone(): ChestGui = super.clone() as ChestGui
+
+ enum class ChestGuiSize(val rows: Int) {
+ ONE_ROW(1),
+ TWO_ROWS(2),
+ THREE_ROWS(3),
+ FOUR_ROWS(4),
+ FIVE_ROWS(5),
+ SIX_ROWS(6);
+
+ companion object {
+ fun fromRows(rows: Int): ChestGuiSize? {
+ return entries.find { it.rows == rows }
+ }
+ }
+ }
+}
+
+fun chestMenu(
+ size: ChestGui.ChestGuiSize = ChestGui.ChestGuiSize.SIX_ROWS,
+ init: ChestGui.() -> Unit,
+): ChestGui {
+ contract {
+ callsInPlace(init, InvocationKind.EXACTLY_ONCE)
+ }
+
+ val gui = InventoryBridge.instance.createChestGui(size)
+ gui.init()
+ return gui
+}
+
+fun Gui.childChestMenu(
+ size: ChestGui.ChestGuiSize = ChestGui.ChestGuiSize.SIX_ROWS,
+ init: ChestGui.() -> Unit,
+): ChestGui {
+ contract {
+ callsInPlace(init, InvocationKind.EXACTLY_ONCE)
+ }
+
+ val gui = InventoryBridge.instance.createChestGui(size, this)
+ gui.init()
+ return gui
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/GuiItem.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/GuiItem.kt
index 3b02b836..cfa34692 100644
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/GuiItem.kt
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/GuiItem.kt
@@ -1,37 +1,38 @@
-@file:OptIn(ExperimentalContracts::class)
-
package dev.slne.surf.surfapi.bukkit.api.inventory.item
-import com.github.stefvanschie.inventoryframework.gui.GuiItem
-import org.bukkit.Material
-import org.bukkit.event.inventory.InventoryClickEvent
+import dev.slne.surf.surfapi.bukkit.api.inventory.InventoryBridge
+import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.GuiItemDsl
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandler
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandlerDsl
+import org.bukkit.NamespacedKey
import org.bukkit.inventory.ItemStack
-import kotlin.contracts.ExperimentalContracts
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
+import org.bukkit.inventory.ItemType
+import java.util.*
-fun guiItem(item: ItemStack, action: InventoryClickEvent.() -> Unit = {}): GuiItem {
- contract {
- callsInPlace(action, InvocationKind.UNKNOWN)
- }
+@GuiItemDsl
+interface GuiItem : Cloneable {
+ val itemStack: ItemStack
+ val key: NamespacedKey
+ val uuid: UUID
+ var visible: Boolean
- return GuiItem(item, action)
-}
+ fun item(itemStack: ItemStack)
+ fun item(type: ItemType, block: ItemStack.() -> Unit = {})
-fun guiItem(
- material: Material,
- item: ItemStack.() -> Unit,
- action: InventoryClickEvent.() -> Unit = {}
-): GuiItem {
- contract {
- callsInPlace(item, InvocationKind.EXACTLY_ONCE)
- callsInPlace(action, InvocationKind.UNKNOWN)
- }
+ fun onClick(handler: ClickHandler)
+ fun onClick(handler: ClickHandlerDsl)
- return GuiItem(ItemStack(material).apply(item), action)
-}
+ public override fun clone(): GuiItem = super.clone() as GuiItem
-fun guiItem(
- material: Material,
- action: InventoryClickEvent.() -> Unit = {}
-) = guiItem(material, {}, action)
\ No newline at end of file
+ companion object {
+ operator fun invoke(
+ key: NamespacedKey? = null,
+ uuid: UUID? = null,
+ init: GuiItem.() -> Unit,
+ ): GuiItem {
+ val item = InventoryBridge.instance.createGuiItem(key, uuid)
+ item.init()
+ return item
+ }
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/SurfGuiItem.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/SurfGuiItem.kt
deleted file mode 100644
index 6a4cf99a..00000000
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/SurfGuiItem.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package dev.slne.surf.surfapi.bukkit.api.inventory.item
-
-import com.github.stefvanschie.inventoryframework.gui.GuiItem
-import dev.slne.surf.surfapi.bukkit.api.inventory.SinglePlayerGui
-import org.bukkit.event.inventory.InventoryClickEvent
-import org.bukkit.inventory.ItemStack
-
-class SurfGuiItem : GuiItem {
-
- constructor(item: ItemStack?) : super(item ?: ItemStack.empty())
- constructor() : super(ItemStack.empty())
-
- var click: InventoryClickEvent.() -> Unit = {}
- set(value) = setAction(value)
-
- var itemPermission: String? = null
- private set
-
- var condition: () -> Boolean = { true }
-
- fun SinglePlayerGui.permission(permission: String) {
- itemPermission = permission
- }
-}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/UpdatableGuiItem.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/UpdatableGuiItem.kt
new file mode 100644
index 00000000..2c275265
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/UpdatableGuiItem.kt
@@ -0,0 +1,27 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.item
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.InventoryBridge
+import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.GuiItemDsl
+import org.bukkit.NamespacedKey
+import java.util.*
+import java.util.function.BooleanSupplier
+
+@GuiItemDsl
+interface UpdatableGuiItem : GuiItem {
+
+ fun onUpdate(update: BooleanSupplier)
+
+ override fun clone(): UpdatableGuiItem = super.clone() as UpdatableGuiItem
+
+ companion object {
+ operator fun invoke(
+ key: NamespacedKey? = null,
+ uuid: UUID? = null,
+ init: UpdatableGuiItem.() -> Unit,
+ ): UpdatableGuiItem {
+ val item = InventoryBridge.instance.createUpdatableGuiItem(key, uuid)
+ item.init()
+ return item
+ }
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/button.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/button.kt
deleted file mode 100644
index 80d60482..00000000
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/item/button.kt
+++ /dev/null
@@ -1,2 +0,0 @@
-package dev.slne.surf.surfapi.bukkit.api.inventory.item
-
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/Pane.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/Pane.kt
new file mode 100644
index 00000000..f16098e2
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/Pane.kt
@@ -0,0 +1,29 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.pane
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandler
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandlerDsl
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Priority
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Slot
+import it.unimi.dsi.fastutil.objects.ObjectList
+import org.jetbrains.annotations.Unmodifiable
+import java.util.*
+
+interface Pane : Cloneable {
+ var slot: Slot
+ val uuid: UUID
+ val items: @Unmodifiable ObjectList
+ val panes: @Unmodifiable ObjectList
+
+ var length: Int
+ var height: Int
+ var priority: Priority
+ var visible: Boolean
+
+ fun onClick(handler: ClickHandler)
+ fun onClick(handler: ClickHandlerDsl)
+
+ fun clear()
+
+ public override fun clone(): Pane
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/SubmitItemPane.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/SubmitItemPane.kt
deleted file mode 100644
index 5d931509..00000000
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/SubmitItemPane.kt
+++ /dev/null
@@ -1,308 +0,0 @@
-package dev.slne.surf.surfapi.bukkit.api.inventory.pane
-
-import com.github.stefvanschie.inventoryframework.gui.GuiItem
-import com.github.stefvanschie.inventoryframework.gui.InventoryComponent
-import com.github.stefvanschie.inventoryframework.gui.type.util.Gui
-import com.github.stefvanschie.inventoryframework.pane.Pane
-import com.github.stefvanschie.inventoryframework.pane.util.Slot
-import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.slot
-import org.bukkit.Material
-import org.bukkit.event.inventory.InventoryAction
-import org.bukkit.event.inventory.InventoryClickEvent
-import org.bukkit.inventory.ItemStack
-import org.jetbrains.annotations.Range
-import kotlin.math.min
-
-
-class SubmitItemPane @JvmOverloads constructor(
- slot: Slot,
- length: @Range(from = 1, to = 6) Int,
- height: @Range(from = 1, to = 6) Int,
- private val filter: ItemStack.() -> Boolean,
- priority: Priority = Priority.NORMAL
-) : Pane(slot, length, height, priority) {
-
- constructor(
- slot: Slot,
- length: @Range(from = 1, to = 6) Int,
- height: @Range(from = 1, to = 6) Int,
- filter: List,
- priority: Priority = Priority.NORMAL
- ) : this(slot, length, height, { filter.contains(type) }, priority)
-
- private val _items = mutableMapOf()
- val submittedItems get() = _items.toMap()
-
- override fun display(
- inventoryComponent: InventoryComponent,
- paneOffsetX: Int,
- paneOffsetY: Int,
- maxLength: Int,
- maxHeight: Int
- ) {
-// val length = min(length, maxLength)
-// val height = min(height, maxHeight)
-//
-// for ((location, item) in _items.filter { (_, item) -> item.isVisible }) {
-// val x = location.getX(getLength())
-// val y = location.getY(getHeight())
-//
-// if (x < 0 || x >= length || y < 0 || y >= height) {
-// continue
-// }
-//
-// val slot = getSlot()
-// val finalRow = slot.getY(maxLength) + y + paneOffsetY
-// val finalColumn = slot.getX(maxLength) + x + paneOffsetX
-//
-// inventoryComponent.setItem(item, finalColumn, finalRow)
-// }
- }
-
- override fun click(
- gui: Gui,
- inventoryComponent: InventoryComponent,
- event: InventoryClickEvent,
- slot: Int,
- paneOffsetX: Int,
- paneOffsetY: Int,
- maxLength: Int,
- maxHeight: Int
- ): Boolean {
- event.apply {
- when (action) {
- InventoryAction.PICKUP_ALL, InventoryAction.PICKUP_SOME, InventoryAction.PICKUP_HALF, InventoryAction.PICKUP_ONE -> {
- val item = currentItem
-
- if (item == null || filter(item)) {
- if (item == null) {
- _items.remove(slot(slot))
- } else {
- if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) {
- _items[slot(slot)] = item
- }
- }
-
- isCancelled = false
- } else {
- isCancelled = true
- }
- }
-
- InventoryAction.PLACE_ALL, InventoryAction.PLACE_SOME, InventoryAction.PLACE_ONE -> {
- val item = cursor
-
- if (filter(item)) {
- val previous = _items[slot(slot)]
- if (previous == null || !previous.isSimilar(item)) {
- if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) {
- _items[slot(slot)] = item
- }
- } else {
- if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) {
- _items[slot(slot)] = previous.clone().apply { amount += item.amount }
- }
- }
-
- isCancelled = false
- } else {
- isCancelled = true
- }
- }
-
- InventoryAction.SWAP_WITH_CURSOR -> {
- val cursorItem = cursor
- val currentItem = currentItem
-
- if (currentItem == null || filter(currentItem)) {
- if (currentItem == null) {
- _items.remove(slot(slot))
- } else {
- if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) {
- _items[slot(slot)] = currentItem
- }
- }
-
- isCancelled = false
- }
-
- if (filter(cursorItem)) {
- val previous = _items[slot(slot)]
- if (previous == null || !previous.isSimilar(cursorItem)) {
- if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) {
- _items[slot(slot)] = cursorItem
- }
- } else {
- if (isInPane(slot, paneOffsetX, paneOffsetY, maxLength, maxHeight, inventoryComponent)) {
- _items[slot(slot)] =
- previous.clone().apply { amount += cursorItem.amount }
- }
- }
-
- isCancelled = false
- } else {
- isCancelled = true
- }
- }
-
- else -> {
- isCancelled = true
- return false
- }
- }
- }
-
- println("current items: $_items")
-
-
-// val itemToCheck = currentItem ?: cursor
-//
-// if (!filter(itemToCheck)) {
-// event.isCancelled = true // Item wird blockiert
-// return false
-// }
-
-
- // Wenn das geklickte Inventar unser Pane ist, fügen wir das Item hinzu oder entfernen es
-// if (event.clickedInventory == event.inventory) {
-// // Wenn das Slot-Item Luft ist, löschen wir es aus der Map
-// if (itemToCheck.type == Material.AIR) {
-// println("Removing item from slot $slot")
-//// _items.remove(slot(slot)) // Stack wird entfernt
-// } else {
-// // Andernfalls fügen wir den Stack hinzu oder aktualisieren ihn
-// println("Adding item to slot $slot with amount ${itemToCheck.amount}")
-//// _items[slot(slot)] = itemToCheck // Gesamter Stack wird gespeichert
-// }
-// } else {
-// // Wenn der Klick im Player-Inventar stattfindet, erlauben wir die Bewegung
-// println("Clicked inventory is player inventory")
-// }
-// event.isCancelled = false // Erlaubt das Bewegen des Items
-
-// event.isCancelled = false // allow the user to move the item
-
- // if item was moved to our pane add it to the submitted items if it was removed from our pane remove it from the submitted items
-// if (event.clickedInventory == event.inventory) {
-// val length = min(length, maxLength)
-// val height = min(height, maxHeight)
-// val paneSlot = getSlot()
-//
-// val xPosition = paneSlot.getX(maxLength)
-// val yPosition = paneSlot.getY(maxLength)
-// val totalLength = inventoryComponent.length
-//
-// val adjustedSlot =
-// slot - (xPosition + paneOffsetX) - totalLength * (yPosition + paneOffsetY)
-// val x = adjustedSlot % totalLength
-// val y = adjustedSlot / totalLength
-
-
- //this isn't our item
-// if (x < 0 || x >= length || y < 0 || y >= height) {
-// return false
-// }
-
-// for (y in 0 until height) {
-// for (x in 0 until length) {
-// val adjustedX = x + paneOffsetX
-// val adjustedY = y + paneOffsetY
-//
-// val adjustedSlot1 = slot - adjustedX - totalLength * adjustedY
-// val adjustedX1 = adjustedSlot1 % totalLength
-// val adjustedY1 = adjustedSlot1 / totalLength
-//
-// if ((adjustedX1 < 0) || (adjustedX1 >= length) || (adjustedY1 < 0) || (adjustedY1 >= height)) {
-// continue
-// }
-//
-// val item = inventoryComponent.getItem(adjustedX1, adjustedY1)
-//
-// // update the item in the map
-// if (item != null) {
-// _items[slot(adjustedX1, adjustedY1)] = item
-// } else {
-// _items.remove(slot(adjustedX1, adjustedY1))
-// }
-// }
-//
-// }
-
- // walk through all slots in the pane
-// for (i in 0 until length * height) {
-//
-// val x1 = i % length
-// val y1 = i / length
-// val adjustedSlot1 = slot - (x1 + paneOffsetX) - totalLength * (y1 + paneOffsetY)
-// val adjustedX = adjustedSlot1 % totalLength
-// val adjustedY = adjustedSlot1 / totalLength
-//
-// if ((x1 < 0) || (x1 >= length) || (y1 < 0) || (y1 >= height)) {
-// continue
-// }
-//
-// val item = inventoryComponent.getItem(adjustedX, adjustedY)
-//
-// // update the item in the map
-// if (item != null) {
-// _items[slot(x1, y1)] = item
-// } else {
-// _items.remove(slot(x1, y1))
-// }
-// }
-
-// println("clicked inventory is inventory")
-// _items.remove(slot(slot))
-// } else {
-// println("clicked inventory is not inventory")
-// _items[slot(slot)] = currentItem ?: cursor
-// }
-
- return true
- }
-
-
- private fun isInPane(
- slot: Int,
- paneOffsetX: Int,
- paneOffsetY: Int,
- maxLength: Int,
- maxHeight: Int,
- inventoryComponent: InventoryComponent
- ): Boolean {
- val length = min(length, maxLength)
- val height = min(height, maxHeight)
-
- val paneSlot = getSlot()
-
- val xPosition = paneSlot.getX(maxLength)
- val yPosition = paneSlot.getY(maxLength)
-
- val totalLength: Int = inventoryComponent.length
-
- val adjustedSlot =
- slot - (xPosition + paneOffsetX) - totalLength * (yPosition + paneOffsetY)
-
- val x = adjustedSlot % totalLength
- val y = adjustedSlot / totalLength
-
- return !(x < 0 || x >= length || y < 0 || y >= height)
- }
-
-
- override fun copy(): Pane {
- val pane = SubmitItemPane(slot, length, height, filter, priority)
-
- pane._items.putAll(_items.map { (slot, item) -> slot to item.clone() }.toMap(pane._items))
- pane.isVisible = isVisible
- pane.onClick = onClick
- pane.uuid = uuid
-
- return pane
- }
-
- override fun getItems(): MutableCollection = mutableListOf()
- override fun getPanes(): MutableCollection = mutableListOf()
- override fun clear() {
- }
-}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/components/PagingButtons.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/components/PagingButtons.kt
new file mode 100644
index 00000000..3c0ca572
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/components/PagingButtons.kt
@@ -0,0 +1,16 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.pane.components
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandlerDsl
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.Pane
+import org.bukkit.inventory.ItemStack
+
+interface PagingButtons: Pane {
+ fun setBackwardsButton(item: GuiItem)
+ fun setBackwardsButton(item: ItemStack, action: ClickHandlerDsl = {})
+
+ fun setForwardsButton(item: GuiItem)
+ fun setForwardsButton(item: ItemStack, action: ClickHandlerDsl = {})
+
+ override fun clone(): PagingButtons
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/panes/OutlinePane.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/panes/OutlinePane.kt
new file mode 100644
index 00000000..3c3d7faa
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/panes/OutlinePane.kt
@@ -0,0 +1,18 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.Pane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Flippable
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Mask
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Orientable
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Rotatable
+
+interface OutlinePane : Pane, Orientable, Flippable, Rotatable {
+
+ fun setItem(index: Int, item: GuiItem)
+ fun addItem(item: GuiItem)
+ fun removeItem(item: GuiItem)
+ fun applyMask(mask: Mask)
+
+ override fun clone(): OutlinePane
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/panes/PaginatedPane.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/panes/PaginatedPane.kt
new file mode 100644
index 00000000..7683893e
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/panes/PaginatedPane.kt
@@ -0,0 +1,20 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.Pane
+import it.unimi.dsi.fastutil.objects.ObjectList
+import org.bukkit.inventory.ItemStack
+
+interface PaginatedPane: Pane {
+ val page: Int
+ val totalPages: Int
+
+ fun page(page: Int)
+ fun previousPage()
+ fun nextPage()
+
+ fun populateWithGuiItems(items: ObjectList)
+ fun populateWithItemStacks(items: ObjectList)
+
+ override fun clone(): PaginatedPane
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/panes/StaticPane.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/panes/StaticPane.kt
new file mode 100644
index 00000000..e2abbe1a
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/panes/StaticPane.kt
@@ -0,0 +1,21 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandlerDsl
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.Pane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Flippable
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Rotatable
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Slot
+import org.bukkit.inventory.ItemStack
+
+interface StaticPane: Pane, Flippable, Rotatable {
+
+ fun setItem(slot: Slot, item: GuiItem)
+ fun setItem(slot: Slot, init: GuiItem.() -> Unit)
+
+ fun fillWith(item: ItemStack, handler: ClickHandlerDsl = {})
+ fun removeItem(item: GuiItem)
+ fun removeItem(slot: Slot)
+
+ override fun clone(): StaticPane
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Flippable.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Flippable.kt
new file mode 100644
index 00000000..be906122
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Flippable.kt
@@ -0,0 +1,8 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils
+
+interface Flippable {
+
+ var flippedVertically: Boolean
+ var flippedHorizontally: Boolean
+
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Mask.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Mask.kt
new file mode 100644
index 00000000..44c22d7b
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Mask.kt
@@ -0,0 +1,19 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.InventoryBridge
+
+interface Mask {
+ val enabledSlots: Int
+ val length: Int
+ val height: Int
+
+ fun setHeight(height: Int): Mask
+ fun setLength(length: Int): Mask
+ fun getColumn(index: Int): BooleanArray
+ fun getRow(index: Int): BooleanArray
+ fun isEnabled(x: Int, y: Int): Boolean
+
+ companion object {
+ operator fun invoke(vararg mask: String): Mask = InventoryBridge.instance.createMask(*mask)
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Orientable.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Orientable.kt
new file mode 100644
index 00000000..3e47ab84
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Orientable.kt
@@ -0,0 +1,11 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils
+
+interface Orientable {
+
+ var orientation: Orientation
+
+ enum class Orientation {
+ HORIZONTAL,
+ VERTICAL
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Pattern.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Pattern.kt
new file mode 100644
index 00000000..bd7e969a
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Pattern.kt
@@ -0,0 +1,21 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.InventoryBridge
+
+interface Pattern {
+ val length: Int
+ val height: Int
+
+ fun setHeight(height: Int): Pattern
+ fun setLength(length: Int): Pattern
+
+ fun getColumn(index: Int): IntArray
+ fun getRow(index: Int): IntArray
+ fun contains(char: Char): Boolean
+ fun getChar(x: Int, y: Int): Char
+
+ companion object {
+ operator fun invoke(vararg pattern: String): Pattern =
+ InventoryBridge.instance.createPattern(*pattern)
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Rotatable.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Rotatable.kt
new file mode 100644
index 00000000..3a0d7c74
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/pane/utils/Rotatable.kt
@@ -0,0 +1,7 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils
+
+interface Rotatable {
+
+ var rotation: Int
+
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/types/SurfChestGui.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/types/SurfChestGui.kt
deleted file mode 100644
index f8e990ac..00000000
--- a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/types/SurfChestGui.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package dev.slne.surf.surfapi.bukkit.api.inventory.types
-
-import com.github.stefvanschie.inventoryframework.adventuresupport.ComponentHolder
-import com.github.stefvanschie.inventoryframework.gui.type.ChestGui
-import com.github.stefvanschie.inventoryframework.gui.type.util.NamedGui
-import dev.slne.surf.surfapi.bukkit.api.inventory.SinglePlayerGui
-import dev.slne.surf.surfapi.bukkit.api.inventory.SurfGui
-import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.MenuMarker
-import net.kyori.adventure.text.Component
-import org.bukkit.entity.Player
-import org.jetbrains.annotations.Range
-
-@MenuMarker
-open class SurfChestGui internal constructor(
- title: Component,
- rows: @Range(from = 2, to = 6) Int = 6,
- override val parent: SurfGui? = null
-) :
- ChestGui(rows, ComponentHolder.of(title)), SurfGui {
- override val gui: NamedGui
- get() = this
-
- init {
- check(rows in 2..6) { "Rows must be between 2 and 6" }
-
- this.setOnBottomClick { event -> event.isCancelled = true }
- this.setOnBottomDrag { event -> event.isCancelled = true }
- this.setOnTopClick { event -> event.isCancelled = true }
- this.setOnTopDrag { event -> event.isCancelled = true }
- }
-}
-
-@MenuMarker
-class SurfChestSinglePlayerGui internal constructor(
- title: Component,
- override val player: Player,
- rows: @Range(from = 2, to = 6) Int = 6,
- override val parent: SurfGui? = null,
-) :
- SurfChestGui(title, rows, parent), SinglePlayerGui
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/utils/InventoryComponent.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/utils/InventoryComponent.kt
new file mode 100644
index 00000000..782b6058
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/utils/InventoryComponent.kt
@@ -0,0 +1,33 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.utils
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.Pane
+import org.bukkit.inventory.Inventory
+import org.bukkit.inventory.ItemStack
+import org.bukkit.inventory.PlayerInventory
+
+interface InventoryComponent : Cloneable {
+ val length: Int
+ val height: Int
+
+ fun addPane(pane: Pane)
+ fun display(inventory: Inventory, offset: Int)
+ fun display(playerInventory: PlayerInventory, offset: Int)
+ fun placeItems(playerInventory: PlayerInventory, offset: Int)
+ fun placeItems(inventory: Inventory, offset: Int)
+
+ fun excludeRows(from: Int, end: Int): InventoryComponent
+ fun hasItem(): Boolean
+ fun hasItem(slot: Slot): Boolean
+ fun getItem(slot: Slot): ItemStack?
+ fun setItem(item: GuiItem, slot: Slot)
+ fun setItem(itemStack: ItemStack, slot: Slot)
+ fun clearItems()
+ fun inBounds(slot: Slot): Boolean
+ fun inBounds(lowerBound: Int, upperBound: Int, value: Int): Boolean
+ fun getPane(index: Int): Pane
+
+ fun display()
+
+ public override fun clone(): InventoryComponent
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/utils/Priority.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/utils/Priority.kt
new file mode 100644
index 00000000..398c8c7f
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/utils/Priority.kt
@@ -0,0 +1,13 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.utils
+
+enum class Priority {
+ LOWEST,
+ LOW,
+ NORMAL,
+ HIGH,
+ HIGHEST,
+ MONITOR;
+
+ fun isLessThan(priority: Priority): Boolean = this.ordinal < priority.ordinal
+ fun isGreaterThan(priority: Priority): Boolean = this.ordinal > priority.ordinal
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/utils/Slot.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/utils/Slot.kt
new file mode 100644
index 00000000..169463d2
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/inventory/utils/Slot.kt
@@ -0,0 +1,44 @@
+package dev.slne.surf.surfapi.bukkit.api.inventory.utils
+
+import org.jetbrains.annotations.Range
+
+interface Slot {
+ fun getX(length: Int): Int
+ fun getY(length: Int): Int
+
+ companion object {
+ fun fromXY(x: Int, y: Int): Slot = XY(x, y)
+ fun fromIndex(index: Int): Slot = Indexed(index)
+
+ operator fun invoke(
+ x: @Range(from = 0, to = 8) Int,
+ y: @Range(from = 0, to = 5) Int,
+ ): Slot {
+ return fromXY(x, y)
+ }
+
+ operator fun invoke(index: Int): Slot {
+ return fromIndex(index)
+ }
+ }
+}
+
+internal data class XY(val x: Int, val y: Int) : Slot {
+ override fun getX(length: Int) = x
+ override fun getY(length: Int) = y
+}
+
+internal data class Indexed(val index: Int) : Slot {
+ override fun getX(length: Int): Int {
+ require(length > 0) { "Length must be greater than 0" }
+ return index % length
+ }
+
+ override fun getY(length: Int): Int {
+ require(length > 0) { "Length must be greater than 0" }
+ return index / length
+ }
+}
+
+fun slot(x: @Range(from = 0, to = 8) Int, y: @Range(from = 0, to = 5) Int) = Slot.fromXY(x, y)
+fun slot(index: Int) = Slot.fromIndex(index)
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/nms/listener/packets/serverbound/ContainerClickPacket.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/nms/listener/packets/serverbound/ContainerClickPacket.kt
new file mode 100644
index 00000000..35e87d2c
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/nms/listener/packets/serverbound/ContainerClickPacket.kt
@@ -0,0 +1,24 @@
+package dev.slne.surf.surfapi.bukkit.api.nms.listener.packets.serverbound
+
+import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap
+import org.bukkit.entity.Player
+import org.bukkit.inventory.InventoryView
+import org.bukkit.inventory.ItemStack
+
+@NmsUseWithCaution
+interface ContainerClickPacket : NmsServerboundPacket {
+ val view: InventoryView
+ val containerId: Int
+ val stateId: Int
+ val slotNumber: Int
+ val buttonNumber: Int
+ val clickType: WindowClickType
+ val changedSlots: Int2ObjectMap
+ val carriedItem: ItemStack
+ val whoClicked: Player
+
+ enum class WindowClickType {
+ PICKUP, QUICK_MOVE, SWAP, CLONE, THROW, QUICK_CRAFT, PICKUP_ALL, UNKNOWN;
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/nms/listener/packets/serverbound/ContainerClosePacket.kt b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/nms/listener/packets/serverbound/ContainerClosePacket.kt
new file mode 100644
index 00000000..06e556c2
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-api/src/main/kotlin/dev/slne/surf/surfapi/bukkit/api/nms/listener/packets/serverbound/ContainerClosePacket.kt
@@ -0,0 +1,8 @@
+package dev.slne.surf.surfapi.bukkit.api.nms.listener.packets.serverbound
+
+import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution
+
+@NmsUseWithCaution
+interface ContainerClosePacket : NmsServerboundPacket {
+ val containerId: Int
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.java b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.java
index e39ffb5b..6a9cf64a 100644
--- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.java
+++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.java
@@ -2,6 +2,7 @@
import dev.jorel.commandapi.CommandAPI;
import dev.slne.surf.surfapi.bukkit.api.packet.listener.SurfBukkitPacketListenerApi;
+import dev.slne.surf.surfapi.bukkit.test.command.GuiCommandKt;
import dev.slne.surf.surfapi.bukkit.test.command.SurfApiTestCommand;
import dev.slne.surf.surfapi.bukkit.test.command.subcommands.reflection.Reflection;
import dev.slne.surf.surfapi.bukkit.test.listener.ChatListener;
@@ -31,6 +32,7 @@ public void onLoad() {
@Override
public void onEnable() {
new SurfApiTestCommand().register();
+ GuiCommandKt.guiCommand();
Reflection.class.getClassLoader(); // initialize Reflection
}
diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/SurfApiTestCommand.java b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/SurfApiTestCommand.java
index 4a4fed60..565028ea 100644
--- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/SurfApiTestCommand.java
+++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/SurfApiTestCommand.java
@@ -10,7 +10,6 @@
import dev.slne.surf.surfapi.bukkit.test.command.subcommands.ScoreboardTest;
import dev.slne.surf.surfapi.bukkit.test.command.subcommands.SmoothTimeSkip;
import dev.slne.surf.surfapi.bukkit.test.command.subcommands.VisualizerTest;
-import dev.slne.surf.surfapi.bukkit.test.command.subcommands.gui.InventoryFrameworkTest;
public class SurfApiTestCommand extends CommandAPICommand {
@@ -27,7 +26,6 @@ public SurfApiTestCommand() {
new ReflectionTest("reflection"),
new PrefixConfigTest("prefixconfig"),
new CommandExceptionTest("commandexception"),
- new InventoryFrameworkTest("inventoryframework"),
new MaxStacksizeTest("maxstacksize"),
new VisualizerTest("visualizer")
);
diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/subcommands/gui/InventoryFrameworkTest.java b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/subcommands/gui/InventoryFrameworkTest.java
deleted file mode 100644
index 8ea0bdf5..00000000
--- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/java/dev/slne/surf/surfapi/bukkit/test/command/subcommands/gui/InventoryFrameworkTest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package dev.slne.surf.surfapi.bukkit.test.command.subcommands.gui;
-
-import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
-import com.github.stefvanschie.inventoryframework.pane.StaticPane;
-import dev.jorel.commandapi.CommandAPICommand;
-import org.bukkit.Material;
-import org.bukkit.inventory.ItemStack;
-
-public class InventoryFrameworkTest extends CommandAPICommand {
-
- public InventoryFrameworkTest(String commandName) {
- super(commandName);
-
- executesPlayer((player, commandArguments) -> {
- final ChestGui testGui = new ChestGui(1, "Test");
- testGui.setOnGlobalClick(event -> event.setCancelled(true));
-
- final StaticPane pane = new StaticPane(0, 0, 9, 1);
- pane.fillWith(ItemStack.of(Material.DIAMOND), event -> {
- event.setCancelled(true);
- event.getWhoClicked().sendMessage("You clicked on a diamond!");
- });
-
- testGui.addPane(pane);
-
- testGui.show(player);
- });
- }
-}
diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/GuiCommand.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/GuiCommand.kt
new file mode 100644
index 00000000..740d0d63
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/GuiCommand.kt
@@ -0,0 +1,11 @@
+package dev.slne.surf.surfapi.bukkit.test.command
+
+import dev.jorel.commandapi.kotlindsl.commandAPICommand
+import dev.jorel.commandapi.kotlindsl.playerExecutor
+import dev.slne.surf.surfapi.bukkit.test.gui.exampleGui
+
+fun guiCommand() = commandAPICommand("gui") {
+ playerExecutor { player, args ->
+ exampleGui().show(player)
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/ExampleGui.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/ExampleGui.kt
new file mode 100644
index 00000000..0c3f0c68
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/gui/ExampleGui.kt
@@ -0,0 +1,81 @@
+package dev.slne.surf.surfapi.bukkit.test.gui
+
+import dev.slne.surf.surfapi.bukkit.api.builder.ItemStack
+import dev.slne.surf.surfapi.bukkit.api.builder.displayName
+import dev.slne.surf.surfapi.bukkit.api.extensions.server
+import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.menu
+import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.paginatedPane
+import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.pagingButtons
+import dev.slne.surf.surfapi.bukkit.api.inventory.dsl.staticPane
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.slot
+import dev.slne.surf.surfapi.bukkit.test.BukkitPluginMain
+import dev.slne.surf.surfapi.core.api.messages.adventure.sendText
+import dev.slne.surf.surfapi.core.api.messages.adventure.text
+import dev.slne.surf.surfapi.core.api.util.toObjectList
+import org.bukkit.Material
+
+fun exampleGui() = menu(text("Test")) {
+ onGlobalDrag { it.isCancelled = true }
+ onGlobalClick { it.isCancelled = true }
+
+ staticPane(slot(1, 0), 1) {
+ updatableItem(slot(0, 0), ItemStack(Material.BARRIER) {
+ displayName {
+ primary("Tolles Item")
+ }
+ }) {
+ onClick {
+ it.whoClicked.sendText {
+ text("You clicked the test item!")
+ }
+ }
+
+ onUpdate {
+ visible = !visible
+ true
+ }
+ }
+
+ item(slot(1, 0), ItemStack(Material.DIAMOND) {
+ displayName {
+ primary("Diamond")
+ }
+ }) {
+ onClick {
+ it.whoClicked.sendText {
+ text("You clicked the diamond!")
+ }
+ }
+ }
+ }
+
+ val paginatedPane = paginatedPane(slot(0, 1), 5) {
+ val items = (1..500).map { index ->
+ ItemStack(Material.STONE) {
+ displayName {
+ primary("Stone Item $index")
+ }
+ }
+ }.toObjectList()
+
+ populateWithItemStacks(items)
+ }
+
+ pagingButtons(slot(0, 0), paginatedPane) {
+ setBackwardsButton(ItemStack(Material.RED_CONCRETE) {
+ displayName {
+ primary("Previous Page")
+ }
+ })
+
+ setForwardsButton(ItemStack(Material.GREEN_CONCRETE) {
+ displayName {
+ primary("Next Page")
+ }
+ })
+ }
+
+ server.scheduler.runTaskTimer(BukkitPluginMain.getInstance(), Runnable {
+// update()
+ }, 20, 20)
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts b/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts
index 3147096f..c86e453d 100644
--- a/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts
+++ b/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts
@@ -16,6 +16,7 @@ tasks.assemble {
kotlin {
compilerOptions {
optIn.add("dev.slne.surf.surfapi.bukkit.api.visualizer.visualizer.ExperimentalVisualizerApi")
+ optIn.add("dev.slne.surf.surfapi.core.api.util.InternalSurfApi")
}
}
@@ -31,7 +32,6 @@ dependencies {
runtimeOnly(libs.scoreboard.library.implementation)
runtimeOnly(libs.scoreboard.library.modern)
paperLibrary(libs.scoreboard.library.api)
- api(libs.inventoryframework)
api(libs.packetevents.spigot)
paperLibrary(libs.guava)
paperLibrary(libs.caffeine)
@@ -107,7 +107,7 @@ fun NamedDomainObjectContainerScope.registerSoft(
name: String,
required: Boolean = false,
joinClassPath: Boolean = true,
- loadOrder: RelativeLoadOrder = RelativeLoadOrder.BEFORE
+ loadOrder: RelativeLoadOrder = RelativeLoadOrder.BEFORE,
) = register(name) {
this.required = required
this.joinClasspath = joinClassPath
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/InventoryBridgeImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/InventoryBridgeImpl.kt
new file mode 100644
index 00000000..10ba2e3a
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/InventoryBridgeImpl.kt
@@ -0,0 +1,89 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory
+
+import com.google.auto.service.AutoService
+import dev.slne.surf.surfapi.bukkit.api.inventory.InventoryBridge
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.Gui
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.types.ChestGui
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.UpdatableGuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.OutlinePane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.PaginatedPane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.StaticPane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Mask
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Pattern
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Slot
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.AbstractGui
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.types.ChestGuiImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.GuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.UpdatableGuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.panes.OutlinePaneImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.panes.PaginatedPaneImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.panes.StaticPaneImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.utils.MaskImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.utils.PatternImpl
+import org.bukkit.NamespacedKey
+import org.bukkit.inventory.Inventory
+import java.util.*
+
+@AutoService(InventoryBridge::class)
+class InventoryBridgeImpl : InventoryBridge {
+ override fun getGuiByInventory(inventory: Inventory): Gui? {
+ return AbstractGui.getGui(inventory)
+ }
+
+ override fun createGuiItem(
+ key: NamespacedKey?,
+ uuid: UUID?,
+ ): GuiItem {
+ return GuiItemImpl(key ?: GuiItemImpl.DEFAULT_KEY, uuid ?: UUID.randomUUID())
+ }
+
+ override fun createUpdatableGuiItem(
+ key: NamespacedKey?,
+ uuid: UUID?,
+ ): UpdatableGuiItem {
+ return UpdatableGuiItemImpl(key ?: GuiItemImpl.DEFAULT_KEY, uuid ?: UUID.randomUUID())
+ }
+
+ override fun createMask(vararg mask: String): Mask {
+ return MaskImpl(*mask)
+ }
+
+ override fun createPattern(vararg pattern: String): Pattern {
+ return PatternImpl(*pattern)
+ }
+
+ override fun createChestGui(
+ size: ChestGui.ChestGuiSize,
+ parent: Gui?,
+ ): ChestGui {
+ return ChestGuiImpl(size, parent as? AbstractGui)
+ }
+
+ override fun createOutlinePane(
+ slot: Slot,
+ length: Int,
+ height: Int,
+ uuid: UUID?,
+ ): OutlinePane {
+ return OutlinePaneImpl(slot, length, height, uuid = uuid ?: UUID.randomUUID())
+ }
+
+ override fun createPaginatedPane(
+ slot: Slot,
+ length: Int,
+ height: Int,
+ uuid: UUID?,
+ ): PaginatedPane {
+ return PaginatedPaneImpl(slot, length, height, uuid = uuid ?: UUID.randomUUID())
+ }
+
+ override fun createStaticPane(
+ slot: Slot,
+ length: Int,
+ height: Int,
+ uuid: UUID?,
+ ): StaticPane {
+ return StaticPaneImpl(slot, length, height, uuid = uuid ?: UUID.randomUUID())
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/dsl/HandlerDslImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/dsl/HandlerDslImpl.kt
new file mode 100644
index 00000000..98999ce5
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/dsl/HandlerDslImpl.kt
@@ -0,0 +1,27 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.dsl
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.*
+import org.bukkit.event.inventory.InventoryClickEvent
+import org.bukkit.event.inventory.InventoryCloseEvent
+import org.bukkit.event.inventory.InventoryDragEvent
+
+@JvmInline
+value class ClickHandlerScopeImpl(private val handler: ClickHandlerDsl) : ClickHandler {
+ override fun handle(event: InventoryClickEvent) {
+ ClickHandlerScope(event).handler()
+ }
+}
+
+@JvmInline
+value class CloseHandlerScopeImpl(private val handler: CloseHandlerDsl) : CloseHandler {
+ override fun handle(event: InventoryCloseEvent) {
+ CloseHandlerScope(event).handler()
+ }
+}
+
+@JvmInline
+value class DragHandlerScopeImpl(private val handler: DragHandlerDsl) : DragHandler {
+ override fun handle(event: InventoryDragEvent) {
+ DragHandlerScope(event).handler()
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/AbstractGui.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/AbstractGui.kt
new file mode 100644
index 00000000..522114d2
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/AbstractGui.kt
@@ -0,0 +1,217 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.Gui
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.*
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.UpdatableGuiItem
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.dsl.ClickHandlerScopeImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.dsl.CloseHandlerScopeImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.dsl.DragHandlerScopeImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.GuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.UpdatableGuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils.PlayerInventoryCache
+import dev.slne.surf.surfapi.core.api.util.logger
+import it.unimi.dsi.fastutil.objects.Object2IntMap
+import org.bukkit.entity.Player
+import org.bukkit.event.inventory.InventoryClickEvent
+import org.bukkit.event.inventory.InventoryCloseEvent
+import org.bukkit.event.inventory.InventoryDragEvent
+import org.bukkit.event.inventory.InventoryEvent
+import org.bukkit.inventory.Inventory
+import org.bukkit.inventory.ItemStack
+import java.util.*
+
+abstract class AbstractGui(override val parent: AbstractGui? = null) : Gui {
+ val cache = PlayerInventoryCache()
+
+ override lateinit var backingInventory: Inventory
+
+ private var onTopClick: ClickHandler? = null
+ private var onBottomClick: ClickHandler? = null
+ private var onGlobalClick: ClickHandler? = null
+ private var onOutsideClick: ClickHandler? = null
+ private var onTopDrag: DragHandler? = null
+ private var onBottomDrag: DragHandler? = null
+ private var onGlobalDrag: DragHandler? = null
+ private var onClose: CloseHandler? = null
+
+ var shouldNavigateToParentOnClose: Boolean = false
+
+ @Volatile
+ var updating: Boolean = false
+ private set
+
+ abstract fun click(event: InventoryClickEvent)
+ abstract fun isPlayerInventoryUsed(): Boolean
+ protected abstract fun updateAllItems(): Object2IntMap
+ protected abstract fun updateItem0(item: UpdatableGuiItemImpl): Int?
+
+ // region Handlers
+ override fun onTopClick(handler: ClickHandler) {
+ onTopClick = handler
+ }
+
+ override fun onTopClick(handler: ClickHandlerDsl) = onTopClick(ClickHandlerScopeImpl(handler))
+
+ override fun onBottomClick(handler: ClickHandler) {
+ onBottomClick = handler
+ }
+
+ override fun onBottomClick(handler: ClickHandlerDsl) =
+ onBottomClick(ClickHandlerScopeImpl(handler))
+
+ override fun onGlobalClick(handler: ClickHandler) {
+ onGlobalClick = handler
+ }
+
+ override fun onGlobalClick(handler: ClickHandlerDsl) =
+ onGlobalClick(ClickHandlerScopeImpl(handler))
+
+ override fun onOutsideClick(handler: ClickHandler) {
+ onOutsideClick = handler
+ }
+
+ override fun onOutsideClick(handler: ClickHandlerDsl) =
+ onOutsideClick(ClickHandlerScopeImpl(handler))
+
+ override fun onTopDrag(handler: DragHandler) {
+ onTopDrag = handler
+ }
+
+ override fun onTopDrag(handler: DragHandlerDsl) = onTopDrag(DragHandlerScopeImpl(handler))
+
+ override fun onBottomDrag(handler: DragHandler) {
+ onBottomDrag = handler
+ }
+
+ override fun onBottomDrag(handler: DragHandlerDsl) = onBottomDrag(DragHandlerScopeImpl(handler))
+
+ override fun onGlobalDrag(handler: DragHandler) {
+ onGlobalDrag = handler
+ }
+
+ override fun onGlobalDrag(handler: DragHandlerDsl) = onGlobalDrag(DragHandlerScopeImpl(handler))
+
+ override fun onClose(handler: CloseHandler) {
+ onClose = handler
+ }
+
+ override fun onClose(handler: CloseHandlerDsl) = onClose(CloseHandlerScopeImpl(handler))
+ // endregion
+
+ override fun navigateToParentOnClose(enabled: Boolean) {
+ shouldNavigateToParentOnClose = enabled
+ }
+
+ override fun update() {
+ if (updating) return
+ updating = true
+
+ val updatedItems = updateAllItems()
+ for ((item, slot) in updatedItems) {
+ if (item.visible) {
+ backingInventory.setItem(slot, item.itemStack)
+ } else {
+ backingInventory.setItem(slot, ItemStack.empty())
+ }
+ }
+
+ for (viewer in viewers) {
+ val cursor = viewer.itemOnCursor
+
+ viewer.setItemOnCursor(ItemStack.empty())
+ show(viewer)
+ viewer.setItemOnCursor(cursor)
+ }
+
+ updating = false
+ }
+
+ override fun updateItem(item: UpdatableGuiItem) {
+ require(item is UpdatableGuiItemImpl) { "Item must be an instance of UpdatableGuiItemImpl" }
+ if (updating) return
+
+ val slot = updateItem0(item) ?: return
+ if (item.visible) {
+ backingInventory.setItem(slot, item.itemStack)
+ } else {
+ backingInventory.setItem(slot, ItemStack.empty())
+ }
+
+ updating = false
+ }
+
+ protected fun addInventory(inventory: Inventory, gui: AbstractGui) {
+ GUI_INVENTORIES[inventory] = gui
+ }
+
+ fun callOnTopClick(event: InventoryClickEvent) {
+ callCallback(onTopClick, event)
+ }
+
+ fun callOnBottomClick(event: InventoryClickEvent) {
+ callCallback(onBottomClick, event)
+ }
+
+ fun callOnGlobalClick(event: InventoryClickEvent) {
+ callCallback(onGlobalClick, event)
+ }
+
+ fun callOnOutsideClick(event: InventoryClickEvent) {
+ callCallback(onOutsideClick, event)
+ }
+
+ fun callOnTopDrag(event: InventoryDragEvent) {
+ callCallback(onTopDrag, event)
+ }
+
+ fun callOnBottomDrag(event: InventoryDragEvent) {
+ callCallback(onBottomDrag, event)
+ }
+
+ fun callOnGlobalDrag(event: InventoryDragEvent) {
+ callCallback(onGlobalDrag, event)
+ }
+
+ fun callOnClose(event: InventoryCloseEvent) {
+ callCallback(onClose, event)
+ }
+
+ private fun callCallback(
+ callback: GuiHandler?,
+ event: E,
+ ) {
+ if (callback == null) return
+
+ try {
+ callback.handle(event)
+ } catch (exception: Throwable) {
+ log.atSevere()
+ .withCause(exception)
+ .log(buildString {
+ append("Exception while handling $callback")
+
+ if (event is InventoryClickEvent) {
+ append(", slot=${event.slot}")
+ }
+ })
+ }
+ }
+
+ override fun navigateToParent(player: Player): Boolean {
+ val parent = parent
+ if (parent == null) return false
+
+ parent.show(player)
+ parent.update()
+ return true
+ }
+
+ override fun walkParents(): Sequence = generateSequence(this) { it.parent }
+
+ companion object {
+ private val log = logger()
+ private val GUI_INVENTORIES = WeakHashMap()
+
+ fun getGui(inventory: Inventory) = GUI_INVENTORIES[inventory]
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/AbstractNamedGui.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/AbstractNamedGui.kt
new file mode 100644
index 00000000..99ea3c1a
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/AbstractNamedGui.kt
@@ -0,0 +1,24 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.NamedGui
+import dev.slne.surf.surfapi.core.api.messages.builder.SurfComponentBuilder
+import net.kyori.adventure.text.Component
+
+abstract class AbstractNamedGui(parent: AbstractGui? = null) :
+ AbstractGui(parent), NamedGui {
+
+ protected var titleDirty = false
+
+ override var title: Component = Component.empty()
+ set(value) {
+ field = value
+ titleDirty = true
+ }
+
+ override fun title(title: Component) {
+ this.title = title
+ }
+
+ override fun title(builder: SurfComponentBuilder.() -> Unit) =
+ title(SurfComponentBuilder(builder))
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/types/ChestGuiImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/types/ChestGuiImpl.kt
new file mode 100644
index 00000000..c165e927
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/types/ChestGuiImpl.kt
@@ -0,0 +1,112 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.types
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.types.ChestGui
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.Pane
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.AbstractGui
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.AbstractNamedGui
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.utils.InventoryBased
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.GuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.UpdatableGuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils.InventoryComponentImpl
+import dev.slne.surf.surfapi.core.api.util.freeze
+import dev.slne.surf.surfapi.core.api.util.mutableObject2IntMapOf
+import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf
+import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf
+import it.unimi.dsi.fastutil.objects.Object2IntMap
+import org.bukkit.Bukkit
+import org.bukkit.entity.Player
+import org.bukkit.event.inventory.InventoryClickEvent
+
+open class ChestGuiImpl(
+ initialSize: ChestGui.ChestGuiSize = ChestGui.ChestGuiSize.FIVE_ROWS,
+ parent: AbstractGui? = null,
+) : AbstractNamedGui(parent), ChestGui, InventoryBased {
+
+ private var rows: Int = initialSize.rows
+ set(value) {
+ if (value == field) return
+
+ inventoryComponent = InventoryComponentImpl(9, value + 4)
+
+ for (pane in inventoryComponent.panes) {
+ inventoryComponent.addPane(pane)
+ }
+
+ sizeDirty = true
+ }
+
+ override val size: ChestGui.ChestGuiSize
+ get() = ChestGui.ChestGuiSize.fromRows(rows) ?: error("Invalid rows count: $rows")
+
+ override var inventoryComponent = InventoryComponentImpl(9, rows + 4)
+ set(value) {
+ field = value
+ panes = value.panes.freeze()
+ }
+
+ private var sizeDirty = false
+
+ final override var panes = inventoryComponent.panes.freeze()
+ private set
+
+ override val items get() = panes.flatMapTo(mutableObjectListOf()) { it.items }.freeze()
+ override val viewers
+ get() = backingInventory.viewers.filterIsInstanceTo(mutableObjectSetOf()).freeze()
+
+ override fun size(size: ChestGui.ChestGuiSize) {
+ if (this.size == size) return
+ rows = size.rows
+ }
+
+ override fun updateAllItems(): Object2IntMap =
+ panes.fold(mutableObject2IntMapOf()) { acc, pane ->
+ acc.apply { putAll(pane.updateItems()) }
+ }
+
+ override fun updateItem0(item: UpdatableGuiItemImpl) =
+ panes.find { it.items.contains(item) }?.updateItem(item)
+
+
+ override fun addPane(pane: Pane) {
+ inventoryComponent.addPane(pane)
+ }
+
+ override fun createInventory() = Bukkit.createInventory(this, rows * 9, title)
+ override fun getInventory() = backingInventory
+
+ override fun show(player: Player) {
+ if (titleDirty || sizeDirty) {
+ backingInventory = createInventory()
+
+ titleDirty = false
+ sizeDirty = false
+ }
+
+ backingInventory.clear()
+
+ val height = inventoryComponent.height
+ inventoryComponent.display()
+
+ val topComponent = inventoryComponent.excludeRows(height - 4, height - 1)
+ val bottomComponent = inventoryComponent.excludeRows(0, height - 5)
+
+ topComponent.placeItems(backingInventory, 0)
+
+ if (bottomComponent.hasItem()) {
+ if (!cache.contains(player)) {
+ cache.storeAndClear(player)
+ }
+
+ bottomComponent.placeItems(player.inventory, 0)
+ }
+
+ player.openInventory(backingInventory)
+ }
+
+ override fun click(event: InventoryClickEvent) {
+ inventoryComponent.click(this, event, event.rawSlot)
+ }
+
+ override fun isPlayerInventoryUsed() =
+ inventoryComponent.excludeRows(0, inventoryComponent.height - 5).hasItem()
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/utils/InventoryBased.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/utils/InventoryBased.kt
new file mode 100644
index 00000000..6ca99acb
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/gui/utils/InventoryBased.kt
@@ -0,0 +1,8 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.utils
+
+import org.bukkit.inventory.Inventory
+import org.bukkit.inventory.InventoryHolder
+
+interface InventoryBased : InventoryHolder {
+ fun createInventory(): Inventory
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/item/GuiItemImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/item/GuiItemImpl.kt
new file mode 100644
index 00000000..127dc048
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/item/GuiItemImpl.kt
@@ -0,0 +1,93 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.item
+
+import com.jeff_media.morepersistentdatatypes.DataType
+import dev.slne.surf.surfapi.bukkit.api.builder.meta
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandler
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandlerDsl
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.util.key
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.dsl.ClickHandlerScopeImpl
+import dev.slne.surf.surfapi.core.api.util.logger
+import org.bukkit.NamespacedKey
+import org.bukkit.event.inventory.InventoryClickEvent
+import org.bukkit.inventory.ItemStack
+import org.bukkit.inventory.ItemType
+import java.util.*
+
+open class GuiItemImpl(
+ override val key: NamespacedKey = DEFAULT_KEY,
+ override val uuid: UUID = UUID.randomUUID(),
+) : GuiItem, Cloneable {
+ private var _itemStack: ItemStack? = null
+ set(value) {
+ field = value
+ applyUuid()
+ }
+
+ override val itemStack: ItemStack
+ get() = _itemStack ?: error("ItemStack is not initialized.")
+
+ override var visible: Boolean = true
+ var action: ClickHandler? = null
+ private set
+
+ override fun onClick(handler: ClickHandler) {
+ action = handler
+ }
+
+ override fun onClick(handler: ClickHandlerDsl) {
+ onClick(ClickHandlerScopeImpl(handler))
+ }
+
+ override fun item(itemStack: ItemStack) {
+ _itemStack = itemStack
+ }
+
+ override fun item(
+ type: ItemType,
+ block: ItemStack.() -> Unit,
+ ) {
+ item(type.createItemStack().apply { block() })
+ }
+
+ fun callAction(event: InventoryClickEvent) {
+ try {
+ action?.handle(event)
+ } catch (exception: Exception) {
+ log.atSevere()
+ .withCause(exception)
+ .log(
+ "Exception while handling click event in inventory '${event.view.title()}', slot=${event.slot}, item=${itemStack.type}"
+ )
+ }
+ }
+
+ fun applyUuid() {
+ itemStack.meta {
+ persistentDataContainer.set(key, DataType.UUID, uuid)
+ }
+ }
+
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as GuiItemImpl
+
+ return uuid == other.uuid
+ }
+
+ override fun hashCode(): Int {
+ return uuid.hashCode()
+ }
+
+ override fun toString(): String {
+ return "GuiItem(itemStack=$_itemStack, action=$action, key=$key, uuid=$uuid, visible=$visible)"
+ }
+
+ companion object {
+ private val log = logger()
+ val DEFAULT_KEY = key("surf-api-uuid")
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/item/UpdatableGuiItemImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/item/UpdatableGuiItemImpl.kt
new file mode 100644
index 00000000..3b3332f6
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/item/UpdatableGuiItemImpl.kt
@@ -0,0 +1,19 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.item
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.UpdatableGuiItem
+import org.bukkit.NamespacedKey
+import java.util.*
+import java.util.function.BooleanSupplier
+
+class UpdatableGuiItemImpl(
+ key: NamespacedKey = DEFAULT_KEY,
+ uuid: UUID = UUID.randomUUID(),
+) : GuiItemImpl(key, uuid), UpdatableGuiItem {
+ private var update: BooleanSupplier? = null
+
+ override fun onUpdate(update: BooleanSupplier) {
+ this.update = update
+ }
+
+ fun update(): Boolean = update?.asBoolean == true
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/listener/GuiListener.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/listener/GuiListener.kt
new file mode 100644
index 00000000..b4bb5a70
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/listener/GuiListener.kt
@@ -0,0 +1,208 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.listener
+
+import com.github.shynixn.mccoroutine.folia.launch
+import com.github.shynixn.mccoroutine.folia.ticks
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.AbstractGui
+import dev.slne.surf.surfapi.bukkit.server.plugin
+import dev.slne.surf.surfapi.core.api.util.freeze
+import dev.slne.surf.surfapi.core.api.util.logger
+import dev.slne.surf.surfapi.core.api.util.mutableObjectSetOf
+import kotlinx.coroutines.delay
+import org.bukkit.entity.Player
+import org.bukkit.event.EventHandler
+import org.bukkit.event.EventPriority
+import org.bukkit.event.Listener
+import org.bukkit.event.entity.EntityPickupItemEvent
+import org.bukkit.event.inventory.*
+import org.bukkit.event.server.PluginDisableEvent
+import org.bukkit.inventory.Inventory
+import java.util.*
+
+object GuiListener : Listener {
+ private val log = logger()
+ private val activeGuiInstances = mutableObjectSetOf()
+
+ @EventHandler(ignoreCancelled = true)
+ fun InventoryClickEvent.onInventoryClick() {
+ val gui = getGui(inventory) ?: return
+
+ val inventory = view.getInventory(rawSlot) ?: run {
+ gui.callOnOutsideClick(this)
+ return
+ }
+
+ gui.callOnGlobalClick(this)
+
+ if (inventory == view.topInventory) {
+ gui.callOnTopClick(this)
+ } else {
+ gui.callOnBottomClick(this)
+ }
+
+ gui.click(this)
+
+ if (isCancelled) {
+ plugin.launch {
+ delay(1.ticks)
+
+ whoClicked.inventory.setItemInOffHand(whoClicked.inventory.itemInOffHand)
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
+ fun EntityPickupItemEvent.onEntityPickupItem() {
+ val player = entity as? Player ?: return
+
+ val gui = getGui(player.openInventory.topInventory)
+ if (gui == null || !gui.isPlayerInventoryUsed()) {
+ return
+ }
+
+ val leftOver = gui.cache.add(player, item.itemStack)
+
+ if (leftOver == 0) {
+ item.remove()
+ } else {
+ val itemStack = item.itemStack
+ itemStack.amount = leftOver
+ item.itemStack = itemStack
+ }
+
+ isCancelled = true
+ }
+
+ @EventHandler
+ fun InventoryDragEvent.onInventoryDrag() {
+ val gui = getGui(inventory) ?: return
+
+ if (rawSlots.size > 1) {
+ var top = false
+ var bottom = false
+
+ for (slot in rawSlots) {
+ val inventory = view.getInventory(slot) ?: continue
+
+ if (inventory == view.topInventory) {
+ top = true
+ } else if (inventory == view.bottomInventory) {
+ bottom = true
+ }
+
+ if (top && bottom) break
+ }
+
+ gui.callOnGlobalDrag(this)
+
+ if (top) {
+ gui.callOnTopDrag(this)
+ } else if (bottom) {
+ gui.callOnBottomDrag(this)
+ }
+ } else {
+ val index = rawSlots.toTypedArray().first()
+ val slotType = view.getSlotType(index)
+ val even = type == DragType.EVEN
+ val clickType = if (even) ClickType.LEFT else ClickType.RIGHT
+ val action = if (even) InventoryAction.PLACE_SOME else InventoryAction.PLACE_ONE
+
+ val previousCursor = view.cursor
+ view.setCursor(oldCursor)
+
+ val clickEvent = InventoryClickEvent(
+ view,
+ slotType,
+ index,
+ clickType,
+ action
+ )
+
+ clickEvent.onInventoryClick()
+
+ if (Objects.equals(view.cursor, oldCursor)) {
+ view.setCursor(previousCursor)
+ }
+
+ isCancelled = clickEvent.isCancelled
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ fun InventoryCloseEvent.onInventoryClose() {
+ val player = player as? Player ?: return
+ val gui = getGui(inventory) ?: return
+
+ val playerInventory = player.inventory
+ playerInventory.setItemInOffHand(playerInventory.itemInOffHand)
+
+ if (!gui.updating) {
+ gui.callOnClose(this)
+ gui.cache.restoreAndForget(player)
+
+ if (gui.viewers.size == 1) {
+ activeGuiInstances.remove(gui)
+ }
+
+ if (gui.shouldNavigateToParentOnClose) {
+ plugin.launch {
+ delay(1.ticks)
+
+ gui.navigateToParent(player)
+ }
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ fun InventoryOpenEvent.onInventoryOpen() {
+ val gui = getGui(inventory) ?: return
+
+ activeGuiInstances.add(gui)
+ }
+
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
+ fun onPluginDisable(event: PluginDisableEvent) {
+ if (plugin != event.plugin) return
+
+ var counter = 0
+ val maxCount = 10
+
+ while (!activeGuiInstances.isEmpty() && counter++ < maxCount) {
+ for (gui in activeGuiInstances.freeze()) {
+ for (viewer in gui.viewers.freeze()) {
+ viewer.closeInventory()
+ }
+ }
+ }
+
+ if (counter == maxCount) {
+ log.atWarning().log(
+ "Unable to close GUIs on plugin disable. GUIs keep getting opened (tried: $maxCount times). " +
+ "This may lead to memory leaks. Please check your code for any issues."
+ )
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ fun TradeSelectEvent.onTradeSelect() {
+ val gui = getGui(inventory) ?: return
+
+ TODO("Implement TradeSelectEvent handling in GUI")
+ }
+
+ private fun getGui(inventory: Inventory): AbstractGui? {
+ val gui = AbstractGui.getGui(inventory)
+
+ if (gui != null) return gui
+
+ val holder = inventory.holder
+
+ if (holder is AbstractGui) {
+ return holder
+ }
+
+ return null
+ }
+
+
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/AbstractPane.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/AbstractPane.kt
new file mode 100644
index 00000000..82651c28
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/AbstractPane.kt
@@ -0,0 +1,118 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane
+
+import com.jeff_media.morepersistentdatatypes.DataType
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandler
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandlerDsl
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.Pane
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Priority
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Slot
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.dsl.ClickHandlerScopeImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.AbstractGui
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.GuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.UpdatableGuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils.InventoryComponentImpl
+import it.unimi.dsi.fastutil.objects.Object2IntMap
+import it.unimi.dsi.fastutil.objects.ObjectList
+import org.bukkit.event.inventory.InventoryClickEvent
+import org.bukkit.inventory.ItemStack
+import java.util.*
+
+abstract class AbstractPane(
+ override var slot: Slot,
+ override var length: Int,
+ override var height: Int,
+ override var priority: Priority = Priority.NORMAL,
+ override val uuid: UUID = UUID.randomUUID(),
+) : Pane {
+ var onClick: ClickHandler? = null
+ override var visible: Boolean = true
+
+ init {
+ require(length >= 0) { "Length must be greater than or equal to 0 (length=$length)" }
+ require(height >= 0) { "Height must be greater than or equal to 0 (height=$height)" }
+ }
+
+ override fun onClick(handler: ClickHandler) {
+ onClick = handler
+ }
+
+ override fun onClick(handler: ClickHandlerDsl) {
+ onClick(ClickHandlerScopeImpl(handler))
+ }
+
+ abstract fun display(
+ component: InventoryComponentImpl,
+ paneOffsetX: Int,
+ paneOffsetY: Int,
+ maxLength: Int,
+ maxHeight: Int,
+ )
+
+ abstract fun updateItems(): Object2IntMap
+ abstract fun updateItem(item: UpdatableGuiItemImpl): Int?
+
+ abstract fun click(
+ gui: AbstractGui,
+ component: InventoryComponentImpl,
+ event: InventoryClickEvent,
+ slot: Int,
+ paneOffsetX: Int,
+ paneOffsetY: Int,
+ maxLength: Int,
+ maxHeight: Int,
+ ): Boolean
+
+ abstract override val items: ObjectList
+ abstract override val panes: ObjectList
+
+ protected fun callOnClick(event: InventoryClickEvent) {
+ try {
+ onClick?.handle(event)
+ } catch (exception: Exception) {
+ throw RuntimeException(buildString {
+ append("Exception while handling click event in inventory '")
+ append(event.view.title())
+ append(
+ "slot=${event.slot}, for ${javaClass.simpleName}, x=${slot.getX(length)}, y=${
+ slot.getY(
+ length
+ )
+ }, length=$length, height=$height"
+ )
+ }, exception)
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ protected fun matchesItem(
+ guiItem: GuiItem,
+ item: ItemStack,
+ ): Boolean {
+ return guiItem.uuid == item.persistentDataContainer.get(guiItem.key, DataType.UUID)
+ }
+
+ @JvmStatic
+ protected fun findMatchingItem(
+ items: Collection,
+ item: ItemStack,
+ ): T? {
+ for (guiItem in items) {
+ if (matchesItem(guiItem, item)) {
+ return guiItem
+ }
+ }
+
+ return null
+ }
+ }
+
+ public override fun clone(): AbstractPane {
+ throw UnsupportedOperationException("The implementing pane has not overridden the clone method.")
+ }
+
+ override fun toString(): String {
+ return "Pane(slot=$slot, length=$length, height=$height, priority=$priority, uuid=$uuid, onClick=$onClick, visible=$visible, items=$items, panes=$panes)"
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/OutlinePaneImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/OutlinePaneImpl.kt
new file mode 100644
index 00000000..c5d75f11
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/OutlinePaneImpl.kt
@@ -0,0 +1,335 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.panes
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.OutlinePane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Mask
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Orientable
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Priority
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Slot
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.AbstractGui
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.GuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.UpdatableGuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.AbstractPane
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.utils.MaskImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils.GeometryUtils
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils.InventoryComponentImpl
+import dev.slne.surf.surfapi.core.api.util.mutableObject2IntMapOf
+import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf
+import dev.slne.surf.surfapi.core.api.util.objectListOf
+import it.unimi.dsi.fastutil.objects.Object2IntMap
+import org.bukkit.event.inventory.InventoryClickEvent
+import java.util.*
+
+class OutlinePaneImpl(
+ slot: Slot,
+ length: Int,
+ height: Int,
+ priority: Priority = Priority.NORMAL,
+ uuid: UUID = UUID.randomUUID(),
+) : AbstractPane(slot, length, height, priority, uuid), OutlinePane {
+
+ override var length: Int = super.length
+ get() = super.length
+ set(value) {
+ field = value
+
+ applyMask(mask.setLength(value))
+ }
+
+ override var height: Int = super.height
+ get() = super.height
+ set(value) {
+ field = value
+
+ applyMask(mask.setHeight(value))
+ }
+
+ override var orientation = Orientable.Orientation.HORIZONTAL
+ override var flippedHorizontally = false
+ override var flippedVertically = false
+ override var rotation = 0
+ set(value) {
+ if (length != height) {
+ throw IllegalArgumentException("Rotation is only allowed for square panes.")
+ }
+
+ if (rotation % 90 != 0) {
+ throw IllegalArgumentException("Rotation must be a multiple of 90 degrees.")
+ }
+
+ field = value % 360
+ }
+
+ private var gap = 0
+ private var repeat = false
+
+ private var alignment = Alignment.BEGIN
+ private var mask: MaskImpl
+
+ override val panes = objectListOf()
+ override val items = mutableObjectListOf(length * height)
+
+ init {
+ val mask = Array(height) { "" }
+ val maskString = buildString {
+ for (i in 0 until length) {
+ append("1")
+ }
+ }
+
+ mask.fill(maskString)
+ this.mask = MaskImpl(*mask)
+ }
+
+ override fun display(
+ component: InventoryComponentImpl,
+ paneOffsetX: Int,
+ paneOffsetY: Int,
+ maxLength: Int,
+ maxHeight: Int,
+ ) {
+ val length = minOf(length, maxLength)
+ val height = minOf(height, maxHeight)
+
+ var itemIndex = 0
+ var gapCount = 0
+
+ val size = when (orientation) {
+ Orientable.Orientation.HORIZONTAL -> {
+ height
+ }
+
+ Orientable.Orientation.VERTICAL -> {
+ length
+ }
+ }
+
+ for (vectorIndex in 0 until size) {
+ if (items.size <= itemIndex) break
+
+ val maskLine = when (orientation) {
+ Orientable.Orientation.HORIZONTAL -> {
+ mask.getRow(vectorIndex)
+ }
+
+ Orientable.Orientation.VERTICAL -> {
+ mask.getColumn(vectorIndex)
+ }
+ }
+
+ var enabled = 0
+ for (bool in maskLine) {
+ if (bool) enabled++
+ }
+
+ val displayItems: Array = if (repeat) {
+ arrayOfNulls(enabled)
+ } else {
+ val remaining = gapCount + (items.size - itemIndex - 1) * (gap + 1) + 1
+
+ arrayOfNulls(minOf(enabled, remaining))
+ }
+
+ for (index in 0 until displayItems.size) {
+ if (gapCount == 0) {
+ displayItems[index] = items[itemIndex]
+
+ itemIndex++
+
+ if (repeat && itemIndex >= items.size) {
+ itemIndex = 0
+ }
+
+ gapCount = gap
+ } else {
+ displayItems[index] = null
+
+ gapCount--
+ }
+ }
+
+ var index = if (alignment == Alignment.BEGIN) {
+ 0
+ } else {
+ -((enabled - displayItems.size) / 2)
+ }
+ for (opposingVectorIndex in 0 until maskLine.size) {
+ if (!maskLine[opposingVectorIndex]) {
+ continue
+ }
+
+ if (index >= 0 && index < displayItems.size && displayItems[index] != null) {
+ var x: Int
+ var y: Int
+
+ when (orientation) {
+ Orientable.Orientation.HORIZONTAL -> {
+ x = opposingVectorIndex
+ y = vectorIndex
+ }
+
+ Orientable.Orientation.VERTICAL -> {
+ x = vectorIndex
+ y = opposingVectorIndex
+ }
+ }
+
+ if (flippedHorizontally) {
+ x = length - x - 1
+ }
+
+ if (flippedVertically) {
+ y = height - y - 1
+ }
+
+ val coordinates = GeometryUtils.processClockwiseRotation(
+ x,
+ y,
+ length,
+ height,
+ rotation
+ )
+
+ x = coordinates.intKey
+ y = coordinates.intValue
+
+ if (x < 0 || x >= length || y < 0 || y >= height) {
+ continue
+ }
+
+ val finalRow = slot.getY(maxLength) + y + paneOffsetY
+ val finalColumn = slot.getX(maxLength) + x + paneOffsetX
+
+ val item = displayItems[index] ?: continue
+ if (item.visible) {
+ component.setItem(item, Slot.fromXY(finalColumn, finalRow))
+ }
+ }
+
+ index++
+ }
+ }
+ }
+
+ override fun updateItem(item: UpdatableGuiItemImpl): Int? {
+ for ((index, guiItem) in items.withIndex()) {
+ if (guiItem != item) continue
+ if (!item.update()) return null
+
+ return index + slot.getX(length) + slot.getY(length) * length
+ }
+
+ return null
+ }
+
+ override fun updateItems(): Object2IntMap {
+ val updatedItems = mutableObject2IntMapOf()
+
+ for ((index, guiItem) in items.withIndex()) {
+ if (guiItem !is UpdatableGuiItemImpl) continue
+ if (!guiItem.update()) continue
+
+ updatedItems[guiItem] = index + slot.getX(length) + slot.getY(length) * length
+ }
+
+ return updatedItems
+ }
+
+ override fun click(
+ gui: AbstractGui,
+ component: InventoryComponentImpl,
+ event: InventoryClickEvent,
+ slot: Int,
+ paneOffsetX: Int,
+ paneOffsetY: Int,
+ maxLength: Int,
+ maxHeight: Int,
+ ): Boolean {
+ val length = minOf(length, maxLength)
+ val height = minOf(height, maxHeight)
+
+ val xPosition = this.slot.getX(maxLength)
+ val yPosition = this.slot.getY(maxLength)
+
+ val totalLength = component.length
+ val adjustedSlot =
+ slot - (xPosition + paneOffsetX) - totalLength * (yPosition + paneOffsetY)
+
+ val x = adjustedSlot % length
+ val y = adjustedSlot / length
+
+ if (x < 0 || x >= length || y < 0 || y >= height) {
+ return false
+ }
+
+ callOnClick(event)
+
+ val itemStack = event.currentItem ?: return false
+ val item = findMatchingItem(items, itemStack) ?: return false
+
+ item.callAction(event)
+
+ return true
+ }
+
+ override fun clone(): OutlinePaneImpl {
+ val outlinePane = OutlinePaneImpl(
+ slot,
+ length,
+ height,
+ priority,
+ uuid
+ )
+
+ for (item in items) {
+ outlinePane.addItem(item.clone())
+ }
+
+ outlinePane.visible = visible
+ outlinePane.onClick = onClick
+
+ outlinePane.orientation = orientation
+ outlinePane.flippedHorizontally = flippedHorizontally
+ outlinePane.flippedVertically = flippedVertically
+ outlinePane.rotation = rotation
+ outlinePane.gap = gap
+ outlinePane.repeat = repeat
+ outlinePane.alignment = alignment
+ outlinePane.mask = mask
+
+ return outlinePane
+ }
+
+ override fun setItem(index: Int, item: GuiItem) {
+ require(item is GuiItemImpl) { "Item must be of type GuiItemImpl" }
+ items.add(index, item)
+ }
+
+ override fun addItem(item: GuiItem) {
+ require(item is GuiItemImpl) { "Item must be of type GuiItemImpl" }
+ items.add(item)
+ }
+
+ override fun removeItem(item: GuiItem) {
+ require(item is GuiItemImpl) { "Item must be of type GuiItemImpl" }
+ items.remove(item)
+ }
+
+ override fun clear() {
+ items.clear()
+ }
+
+ override fun applyMask(mask: Mask) {
+ require(mask is MaskImpl) { "Mask must be of type MaskImpl" }
+ if (length != mask.length || height != mask.height) {
+ throw IllegalArgumentException("Mask dimensions must match pane dimensions.")
+ }
+
+ this.mask = mask
+ }
+
+ enum class Alignment {
+ BEGIN, CENTER
+ }
+
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/PaginatedPaneImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/PaginatedPaneImpl.kt
new file mode 100644
index 00000000..7fa69a09
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/PaginatedPaneImpl.kt
@@ -0,0 +1,284 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.panes
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.Pane
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.PaginatedPane
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Priority
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Slot
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.AbstractGui
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.GuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.UpdatableGuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.AbstractPane
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils.InventoryComponentImpl
+import dev.slne.surf.surfapi.core.api.util.*
+import it.unimi.dsi.fastutil.objects.Object2IntMap
+import it.unimi.dsi.fastutil.objects.ObjectList
+import org.bukkit.event.inventory.InventoryClickEvent
+import org.bukkit.inventory.ItemStack
+import java.util.*
+import kotlin.math.ceil
+
+class PaginatedPaneImpl(
+ slot: Slot,
+ length: Int,
+ height: Int,
+ priority: Priority = Priority.NORMAL,
+ uuid: UUID = UUID.randomUUID(),
+) : AbstractPane(slot, length, height, priority, uuid), PaginatedPane {
+
+ override val totalPages: Int
+ get() = paginatedPanes.size
+
+ override val panes: ObjectList
+ get() {
+ val panes = mutableObjectListOf()
+
+ paginatedPanes.forEach { (_, paginatedPane) ->
+ paginatedPane.forEach { pane ->
+ panes.addAll(pane.panes)
+ }
+ panes.addAll(paginatedPane)
+ }
+
+ return panes
+ }
+
+ override val items: ObjectList
+ get() = panes.flatMap { it.items }.toObjectList()
+
+ override fun clear() {
+ paginatedPanes.clear()
+ }
+
+ private val paginatedPanes = mutableInt2ObjectMapOf>()
+ override var page: Int = 0
+ set(value) {
+ if (!paginatedPanes.containsKey(value)) {
+ throw IllegalArgumentException("Page $value does not exist.")
+ }
+
+ field = value
+ }
+
+ override fun page(page: Int) {
+ this.page = page
+ }
+
+ override fun previousPage() {
+ if (page > 0) {
+ page--
+ }
+ }
+
+ override fun nextPage() {
+ if (paginatedPanes.containsKey(page + 1)) {
+ page++
+ }
+ }
+
+ fun addPage(pane: Pane) {
+ require(pane is AbstractPane) { "Only AbstractPane can be added to PaginatedPane." }
+ val newPageList = mutableObjectListOf(pane)
+
+ if (paginatedPanes.isEmpty()) {
+ paginatedPanes[0] = newPageList
+ return
+ }
+
+ val highestPage = paginatedPanes.keys.maxOrNull() ?: 0
+
+ if (highestPage == Int.MAX_VALUE) {
+ throw ArithmeticException("Cannot add more pages, maximum reached.")
+ }
+
+ paginatedPanes[highestPage + 1] = newPageList
+ }
+
+ fun addPane(page: Int, pane: Pane) {
+ require(pane is AbstractPane) { "Only AbstractPane can be added to PaginatedPane." }
+
+ if (!paginatedPanes.containsKey(page)) {
+ paginatedPanes[page] = mutableObjectListOf()
+ }
+
+ paginatedPanes[page]!!.add(pane)
+ paginatedPanes[page]!!.sortWith(Comparator.comparing(AbstractPane::priority))
+ }
+
+ override fun populateWithGuiItems(items: ObjectList) {
+ if (items.isEmpty()) {
+ return
+ }
+
+ val itemsPerPage = length * height
+ val pagesNeeded: Int = ceil(items.size / itemsPerPage.toDouble()).coerceAtLeast(1.0).toInt()
+
+ for (i in 0 until pagesNeeded) {
+ val page = OutlinePaneImpl(Slot(0, 0), length, height)
+
+ for (j in 0 until itemsPerPage) {
+ val index = i * itemsPerPage + j
+
+ if (index >= items.size) {
+ break
+ }
+
+ page.addItem(items[index])
+ }
+
+ addPane(i, page)
+ }
+ }
+
+ override fun populateWithItemStacks(items: ObjectList) =
+ populateWithGuiItems(items.map { GuiItemImpl().apply { item(it) } }.toObjectList())
+
+ override fun display(
+ component: InventoryComponentImpl,
+ paneOffsetX: Int,
+ paneOffsetY: Int,
+ maxLength: Int,
+ maxHeight: Int,
+ ) {
+ val panes = paginatedPanes[page] ?: return
+
+ for (pane in panes) {
+ if (!pane.visible) {
+ continue
+ }
+
+ val newPaneOffsetX = paneOffsetX + slot.getX(maxLength)
+ val newPaneOffsetY = paneOffsetY + slot.getY(maxLength)
+ val newMaxLength = minOf(length, maxLength)
+ val newMaxHeight = minOf(height, maxHeight)
+
+ pane.display(component, newPaneOffsetX, newPaneOffsetY, newMaxLength, newMaxHeight)
+ }
+ }
+
+ override fun updateItems(): Object2IntMap {
+ val updatedItems = mutableObject2IntMapOf()
+ val selectedPagePanes = paginatedPanes[page] ?: return updatedItems
+
+ for (pane in selectedPagePanes) {
+ if (!pane.visible) {
+ continue
+ }
+
+ val paneItems = pane.updateItems()
+ for ((item, index) in paneItems) {
+ updatedItems[item] =
+ index + pane.slot.getX(length) + pane.slot.getY(length) * length
+ }
+ }
+
+ return updatedItems
+ }
+
+ override fun updateItem(item: UpdatableGuiItemImpl): Int? {
+ val selectedPagePanes = paginatedPanes[page] ?: return null
+
+ for (pane in selectedPagePanes) {
+ if (!pane.visible) {
+ continue
+ }
+
+ val index = pane.updateItem(item) ?: continue
+ return index + pane.slot.getX(length) + pane.slot.getY(length) * length
+ }
+
+ return null
+ }
+
+ override fun click(
+ gui: AbstractGui,
+ component: InventoryComponentImpl,
+ event: InventoryClickEvent,
+ slot: Int,
+ paneOffsetX: Int,
+ paneOffsetY: Int,
+ maxLength: Int,
+ maxHeight: Int,
+ ): Boolean {
+ val length = minOf(length, maxLength)
+ val height = minOf(height, maxHeight)
+
+ val xPosition = this.slot.getX(maxLength)
+ val yPosition = this.slot.getY(maxLength)
+
+ val totalLength = component.length
+ val adjustedSlot =
+ slot - (xPosition + paneOffsetX) - totalLength * (yPosition + paneOffsetY)
+
+ val x = adjustedSlot % length
+ val y = adjustedSlot / length
+
+ if (x < 0 || x >= length || y < 0 || y >= height) {
+ return false
+ }
+
+ callOnClick(event)
+
+ for (pane in paginatedPanes.getOrElse(page) { mutableObjectListOf() }.freeze()) {
+ if (!pane.visible) {
+ continue
+ }
+
+ if (pane.click(
+ gui,
+ component,
+ event,
+ slot,
+ paneOffsetX + xPosition,
+ paneOffsetY + yPosition,
+ length,
+ height
+ )
+ ) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ override fun clone(): PaginatedPaneImpl {
+ val paginatedPane = PaginatedPaneImpl(
+ slot,
+ length,
+ height,
+ priority,
+ uuid
+ )
+
+ paginatedPanes.forEach { (pageNumber, panes) ->
+ panes.forEach { pane ->
+ paginatedPane.addPane(pageNumber, pane.clone())
+ }
+ }
+
+ paginatedPane.page = page
+ paginatedPane.visible = visible
+ paginatedPane.onClick = onClick
+
+ return paginatedPane
+ }
+
+ fun removePage(page: Int) {
+ if (paginatedPanes.remove(page) == null) {
+ return
+ }
+
+ val newPanes = mutableInt2ObjectMapOf>()
+ for ((index, panes) in paginatedPanes) {
+ if (index > page) {
+ newPanes.put(index - 1, panes)
+ } else {
+ newPanes.put(index, panes)
+ }
+ }
+
+ paginatedPanes.clear()
+ paginatedPanes.putAll(newPanes)
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/StaticPaneImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/StaticPaneImpl.kt
new file mode 100644
index 00000000..dd05218b
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/StaticPaneImpl.kt
@@ -0,0 +1,219 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.panes
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandlerDsl
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.panes.StaticPane
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Priority
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Slot
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.slot
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.AbstractGui
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.GuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.UpdatableGuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.AbstractPane
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils.GeometryUtils
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils.InventoryComponentImpl
+import dev.slne.surf.surfapi.core.api.util.mutableObject2IntMapOf
+import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf
+import dev.slne.surf.surfapi.core.api.util.objectListOf
+import dev.slne.surf.surfapi.core.api.util.toObjectList
+import it.unimi.dsi.fastutil.objects.Object2IntMap
+import it.unimi.dsi.fastutil.objects.ObjectList
+import org.bukkit.event.inventory.InventoryClickEvent
+import org.bukkit.inventory.ItemStack
+import java.util.*
+
+class StaticPaneImpl(
+ slot: Slot,
+ length: Int,
+ height: Int,
+ priority: Priority = Priority.NORMAL,
+ uuid: UUID = UUID.randomUUID(),
+) : AbstractPane(slot, length, height, priority, uuid), StaticPane {
+
+ override var flippedHorizontally: Boolean = false
+ override var flippedVertically: Boolean = false
+ override var rotation: Int = 0
+ set(value) {
+ if (length != height) {
+ throw IllegalArgumentException("Rotation is only allowed for square panes.")
+ }
+
+ if (rotation % 90 != 0) {
+ throw IllegalArgumentException("Rotation must be a multiple of 90 degrees.")
+ }
+
+ field = value % 360
+ }
+
+ override val panes: ObjectList = objectListOf()
+
+ private val paneItems = mutableObject2ObjectMapOf()
+ override val items: ObjectList get() = paneItems.values.toObjectList()
+
+ override fun display(
+ component: InventoryComponentImpl,
+ paneOffsetX: Int,
+ paneOffsetY: Int,
+ maxLength: Int,
+ maxHeight: Int,
+ ) {
+ val length = minOf(this.length, maxLength)
+ val height = minOf(this.height, maxHeight)
+
+ paneItems.entries.asSequence()
+ .filter { it.value.visible }
+ .forEach { (slot, guiItem) ->
+ var x = slot.getX(length)
+ var y = slot.getY(length)
+
+ if (flippedHorizontally) {
+ x = length - x - 1
+ }
+
+ if (flippedVertically) {
+ y = height - y - 1
+ }
+
+ val coordinates = GeometryUtils.processClockwiseRotation(
+ x,
+ y,
+ length,
+ height,
+ rotation
+ )
+
+ x = coordinates.intKey
+ y = coordinates.intValue
+
+ if (x < 0 || x >= length || y < 0 || y >= height) {
+ return@forEach
+ }
+
+ val finalRow = this@StaticPaneImpl.slot.getY(maxLength) + y + paneOffsetY
+ val finalColumn = this@StaticPaneImpl.slot.getX(maxLength) + x + paneOffsetX
+
+ component.setItem(guiItem, slot(finalColumn, finalRow))
+ }
+ }
+
+ override fun updateItems(): Object2IntMap {
+ val updatedItems = mutableObject2IntMapOf()
+
+ for ((slot, guiItem) in paneItems.entries) {
+ if (guiItem !is UpdatableGuiItemImpl) continue
+ if (!guiItem.update()) continue
+
+ val index = slot.getX(length) + slot.getY(length) * length
+ updatedItems[guiItem] = index
+ }
+
+ return updatedItems
+ }
+
+ override fun updateItem(item: UpdatableGuiItemImpl): Int? {
+ for ((slot, other) in paneItems.entries) {
+ if (other != item) continue
+ if (!item.update()) return null
+ return slot.getX(length) + slot.getY(length) * length
+ }
+ return null
+ }
+
+ override fun click(
+ gui: AbstractGui,
+ component: InventoryComponentImpl,
+ event: InventoryClickEvent,
+ slot: Int,
+ paneOffsetX: Int,
+ paneOffsetY: Int,
+ maxLength: Int,
+ maxHeight: Int,
+ ): Boolean {
+ val length = minOf(this.length, maxLength)
+ val height = minOf(this.height, maxHeight)
+
+ val xPosition = this.slot.getX(maxLength)
+ val yPosition = this.slot.getY(maxLength)
+ val totalLength = component.length
+
+ val adjustedSlot =
+ slot - (xPosition + paneOffsetX) - totalLength * (yPosition + paneOffsetY)
+
+ val x = adjustedSlot % length
+ val y = adjustedSlot / length
+
+ if (x < 0 || x >= length || y < 0 || y >= height) {
+ return false
+ }
+
+ callOnClick(event)
+
+ val itemStack = event.currentItem ?: return false
+ val clickedItem = findMatchingItem(paneItems.values, itemStack) ?: return false
+
+ clickedItem.callAction(event)
+
+ return true
+ }
+
+ override fun clone(): StaticPaneImpl {
+ val clonedPane = StaticPaneImpl(slot, length, height, priority, uuid)
+
+ for (entry in paneItems.entries) {
+ clonedPane.setItem(entry.key, entry.value.clone())
+ }
+
+ clonedPane.visible = visible
+ clonedPane.onClick = onClick
+ clonedPane.flippedHorizontally = flippedHorizontally
+ clonedPane.flippedVertically = flippedVertically
+ clonedPane.rotation = rotation
+
+ return clonedPane
+ }
+
+ override fun fillWith(item: ItemStack, handler: ClickHandlerDsl) {
+ val locations = paneItems.keys
+
+ for (y in 0 until height) {
+ for (x in 0 until length) {
+ var found = false
+
+ for (location in locations) {
+ if (location.getX(length) == x && location.getY(length) == y) {
+ found = true
+ break
+ }
+ }
+
+ if (!found) {
+ setItem(slot(x, y), GuiItemImpl().apply {
+ item(item)
+ onClick(handler)
+ })
+ }
+ }
+ }
+ }
+
+ override fun setItem(slot: Slot, item: GuiItem) {
+ require(item is GuiItemImpl) { "Item must be an instance of GuiItemImpl" }
+ paneItems.put(slot, item)
+ }
+
+ override fun setItem(slot: Slot, init: GuiItem.() -> Unit) {
+ setItem(slot, GuiItemImpl().apply(init))
+ }
+
+ override fun removeItem(item: GuiItem) {
+ paneItems.values.removeIf { guiItem -> guiItem == item }
+ }
+
+ override fun removeItem(slot: Slot) {
+ paneItems.remove(slot)
+ }
+
+ override fun clear() {
+ paneItems.clear()
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/components/PagingButtonsImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/components/PagingButtonsImpl.kt
new file mode 100644
index 00000000..d57e8cd4
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/panes/components/PagingButtonsImpl.kt
@@ -0,0 +1,183 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.panes.components
+
+import dev.slne.surf.surfapi.bukkit.api.builder.displayName
+import dev.slne.surf.surfapi.bukkit.api.inventory.gui.handlers.ClickHandlerDsl
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.components.PagingButtons
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Priority
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Slot
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.slot
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.AbstractGui
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.GuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.item.UpdatableGuiItemImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.AbstractPane
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.panes.PaginatedPaneImpl
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils.InventoryComponentImpl
+import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf
+import dev.slne.surf.surfapi.core.api.util.object2IntMapOf
+import it.unimi.dsi.fastutil.objects.Object2IntMap
+import it.unimi.dsi.fastutil.objects.ObjectList
+import org.bukkit.event.inventory.InventoryClickEvent
+import org.bukkit.inventory.ItemStack
+import org.bukkit.inventory.ItemType
+import java.util.*
+
+class PagingButtonsImpl(
+ slot: Slot,
+ val paginatedPane: PaginatedPaneImpl,
+ length: Int = 9,
+ priority: Priority = Priority.NORMAL,
+ uuid: UUID = UUID.randomUUID(),
+) : AbstractPane(slot, length, 1, priority, uuid), PagingButtons {
+
+ override val items: ObjectList
+ get() = mutableObjectListOf(backwardsButton, forwardsButton)
+
+ override val panes: ObjectList
+ get() = mutableObjectListOf()
+
+ override fun clear() {
+ throw UnsupportedOperationException("PagingButtons cannot be cleared.")
+ }
+
+ var backwardsButton = GuiItemImpl().apply {
+ item(ItemType.ARROW) {
+ displayName {
+ primary("Backwards")
+ }
+ }
+ }
+
+ override fun setBackwardsButton(item: GuiItem) {
+ require(item is GuiItemImpl) { "Backwards button must be an instance of GuiItemImpl." }
+ this.backwardsButton = item
+ }
+
+ override fun setBackwardsButton(item: ItemStack, action: ClickHandlerDsl) {
+ setBackwardsButton(GuiItem {
+ item(item)
+ onClick(action)
+ })
+ }
+
+ var forwardsButton = GuiItemImpl().apply {
+ item(ItemType.ARROW) {
+ displayName {
+ primary("Forwards")
+ }
+ }
+ }
+
+ override fun setForwardsButton(item: GuiItem) {
+ require(item is GuiItemImpl) { "Forwards button must be an instance of GuiItemImpl." }
+ this.forwardsButton = item
+ }
+
+ override fun setForwardsButton(item: ItemStack, action: ClickHandlerDsl) {
+ setForwardsButton(GuiItem {
+ item(item)
+ onClick(action)
+ })
+ }
+
+ override fun click(
+ gui: AbstractGui,
+ component: InventoryComponentImpl,
+ event: InventoryClickEvent,
+ slot: Int,
+ paneOffsetX: Int,
+ paneOffsetY: Int,
+ maxLength: Int,
+ maxHeight: Int,
+ ): Boolean {
+ val length = minOf(length, maxLength)
+ val height = minOf(height, maxHeight)
+
+ val xPosition = this.slot.getX(maxLength)
+ val yPosition = this.slot.getY(maxLength)
+
+ val totalLength = component.length
+ val adjustedSlot =
+ slot - (xPosition + paneOffsetX) - totalLength * (yPosition + paneOffsetY)
+
+ val x = adjustedSlot % length
+ val y = adjustedSlot / length
+
+ if (x < 0 || x >= length || y < 0 || y >= height) {
+ return false
+ }
+
+ callOnClick(event)
+
+ val itemStack = event.currentItem ?: return false
+
+ if (matchesItem(backwardsButton, itemStack)) {
+ paginatedPane.previousPage()
+ backwardsButton.callAction(event)
+ gui.update()
+
+ return true
+ }
+
+ if (matchesItem(forwardsButton, itemStack)) {
+ paginatedPane.nextPage()
+ forwardsButton.callAction(event)
+ gui.update()
+
+ return true
+ }
+
+ return false
+ }
+
+ override fun display(
+ component: InventoryComponentImpl,
+ paneOffsetX: Int,
+ paneOffsetY: Int,
+ maxLength: Int,
+ maxHeight: Int,
+ ) {
+ val length = length.coerceAtMost(maxLength)
+
+ val x = super.slot.getX(length) + paneOffsetX
+ val y = super.slot.getY(length) + paneOffsetY
+
+ if (paginatedPane.page > 0) {
+ component.setItem(backwardsButton, slot(x, y))
+ }
+
+ if (paginatedPane.page < paginatedPane.totalPages - 1) {
+ component.setItem(forwardsButton, slot(x + length - 1, y))
+ }
+ }
+
+ override fun updateItems(): Object2IntMap = object2IntMapOf()
+
+ override fun updateItem(item: UpdatableGuiItemImpl): Int? {
+ return null
+ }
+
+ override fun clone(): PagingButtonsImpl {
+ val pagingButtons = PagingButtonsImpl(
+ slot,
+ paginatedPane,
+ length,
+ priority,
+ uuid
+ )
+
+ pagingButtons.visible = visible
+ pagingButtons.onClick = onClick
+ pagingButtons.backwardsButton = backwardsButton.clone()
+ pagingButtons.forwardsButton = forwardsButton.clone()
+
+ return pagingButtons
+ }
+
+ init {
+ if (length < 2) {
+ throw IllegalArgumentException("Length must be at least 2 to accommodate paging buttons.")
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/utils/MaskImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/utils/MaskImpl.kt
new file mode 100644
index 00000000..07147e60
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/pane/utils/MaskImpl.kt
@@ -0,0 +1,128 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.utils
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.utils.Mask
+import java.util.*
+import kotlin.math.min
+
+class MaskImpl(private val mask: Array) : Mask {
+
+ override val enabledSlots: Int get() = amountOfEnabledSlots()
+ override val length: Int get() = mask[0].size
+ override val height: Int get() = mask.size
+
+ constructor(vararg mask: String) : this(maskByString(*mask))
+
+ companion object {
+ private fun maskByString(vararg mask: String): Array {
+ val maskArray = Array(mask.size) {
+ BooleanArray(if (mask.isEmpty()) 0 else mask[0].length)
+ }
+
+ for (row in mask.indices) {
+ val length = mask[row].length
+
+ require(length == maskArray[row].size) { "Lengths of each string should be equal" }
+
+ for (column in 0.. {
+ maskArray[row][column] = false
+ }
+
+ '1' -> {
+ maskArray[row][column] = true
+ }
+
+ else -> {
+ throw IllegalArgumentException("Strings may only contain '0' and '1'")
+ }
+ }
+ }
+ }
+
+ return maskArray
+ }
+
+ }
+
+ override fun setHeight(height: Int): MaskImpl {
+ val newRows = Array(height) { BooleanArray(this.length) }
+
+ for (index in 0..) : Pattern {
+ constructor(vararg pattern: String) : this(initializePattern(*pattern))
+
+ override val length: Int
+ get() = pattern[0].size
+
+ override val height: Int
+ get() = pattern.size
+
+ override fun setHeight(height: Int): PatternImpl {
+ require(height > 0) { "Height must be greater than 0" }
+
+ val newRows = Array(height) { IntArray(length) }
+
+ for (index in 0 until minOf(height, this.height)) {
+ System.arraycopy(pattern[index], 0, newRows[index], 0, pattern[index].size)
+ }
+
+ for (index in minOf(height, this.height) until height) {
+ val previousRow = newRows[index - 1]
+
+ newRows[index] = previousRow.copyOf(previousRow.size)
+ }
+
+ return PatternImpl(newRows)
+ }
+
+ override fun setLength(length: Int): PatternImpl {
+ require(length > 0) { "Length must be greater than 0" }
+
+ val newRows = Array(height) { IntArray(length) }
+
+ for (index in 0 until height) {
+ val newRow = IntArray(length)
+ val row = pattern[index]
+ val minLength = minOf(length, row.size)
+
+ System.arraycopy(row, 0, newRow, 0, minLength)
+
+ for (column in minLength until length) {
+ newRow[column] = newRow[minLength - 1]
+ }
+
+ newRows[index] = newRow
+ }
+
+ return PatternImpl(newRows)
+ }
+
+ override fun getColumn(index: Int): IntArray {
+ if (index < 0 || index >= length) {
+ throw IndexOutOfBoundsException("Column index $index is out of bounds for pattern of length $length")
+ }
+
+ val column = IntArray(height)
+
+ for (i in 0 until height) {
+ column[i] = pattern[i][index]
+ }
+
+ return column
+ }
+
+ override fun contains(char: Char): Boolean {
+ for (row in pattern) {
+ for (cell in row) {
+ if (cell != char.code) {
+ continue
+ }
+
+ return true
+ }
+ }
+
+ return false
+ }
+
+ override fun getRow(index: Int): IntArray {
+ if (index < 0 || index >= height) {
+ throw IndexOutOfBoundsException("Row index $index is out of bounds for pattern of height $height")
+ }
+
+ val row = pattern[index]
+
+ return row.copyOf(row.size)
+ }
+
+ override fun getChar(x: Int, y: Int): Char {
+ if (x < 0 || x >= length || y < 0 || y >= height) {
+ throw IndexOutOfBoundsException("Coordinates ($x, $y) are out of bounds for pattern of size ${length}x${height}")
+ }
+
+ return pattern[y][x].toChar()
+ }
+
+ override fun hashCode() = pattern.contentDeepHashCode()
+
+ override fun toString() = buildString {
+ append("Pattern{")
+ append("pattern=")
+ append(pattern.contentDeepToString())
+ append('}')
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is PatternImpl) return false
+
+ return pattern.contentDeepEquals(other.pattern)
+ }
+
+ companion object {
+ private fun initializePattern(vararg pattern: String): Array {
+ val rows = pattern.size
+ val zeroRows = rows == 0
+
+ val patternArray = Array(rows) {
+ IntArray(
+ if (zeroRows) 0 else pattern[0].codePointCount(
+ 0,
+ pattern[0].length
+ )
+ )
+ }
+
+ if (zeroRows) {
+ return patternArray
+ }
+
+ val globalLength = pattern[0].length
+
+ for (index in 0 until rows) {
+ val row = pattern[index]
+ val length = row.codePointCount(0, row.length)
+
+ if (length != globalLength) {
+ throw IllegalArgumentException("All rows must have the same length. Row $index has length $length, expected $globalLength")
+ }
+
+ val values = mutableIntListOf()
+ row.codePoints().forEach(values::add)
+
+ for (column in 0 until values.size) {
+ patternArray[index][column] = values.getInt(column)
+ }
+ }
+
+ return patternArray
+ }
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/utils/GeometryUtils.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/utils/GeometryUtils.kt
new file mode 100644
index 00000000..dec05dca
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/utils/GeometryUtils.kt
@@ -0,0 +1,44 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils
+
+import it.unimi.dsi.fastutil.ints.AbstractInt2IntMap
+import it.unimi.dsi.fastutil.ints.Int2IntMap
+
+object GeometryUtils {
+
+ fun processClockwiseRotation(
+ x: Int,
+ y: Int,
+ length: Int,
+ height: Int,
+ rotation: Int,
+ ): Int2IntMap.Entry {
+ var newX = x
+ var newY = y
+
+ when (rotation) {
+ 90 -> {
+ newX = height - 1 - y
+ newY = x
+ }
+
+ 180 -> {
+ newX = length - 1 - x
+ newY = height - 1 - y
+ }
+
+ 270 -> {
+ newX = y
+ newY = length - 1 - x
+ }
+ }
+
+ return AbstractInt2IntMap.BasicEntry(newX, newY)
+ }
+
+ fun processCounterClockwiseRotation(
+ x: Int, y: Int, length: Int, height: Int,
+ rotation: Int,
+ ): MutableMap.MutableEntry {
+ return processClockwiseRotation(x, y, length, height, 360 - rotation)
+ }
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/utils/InventoryComponentImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/utils/InventoryComponentImpl.kt
new file mode 100644
index 00000000..f207bf96
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/utils/InventoryComponentImpl.kt
@@ -0,0 +1,253 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils
+
+import dev.slne.surf.surfapi.bukkit.api.inventory.item.GuiItem
+import dev.slne.surf.surfapi.bukkit.api.inventory.pane.Pane
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.InventoryComponent
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.Slot
+import dev.slne.surf.surfapi.bukkit.api.inventory.utils.slot
+import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.gui.AbstractGui
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.pane.AbstractPane
+import dev.slne.surf.surfapi.core.api.util.freeze
+import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf
+import org.bukkit.event.inventory.InventoryClickEvent
+import org.bukkit.inventory.Inventory
+import org.bukkit.inventory.ItemStack
+import org.bukkit.inventory.PlayerInventory
+
+class InventoryComponentImpl(
+ override val length: Int,
+ override val height: Int,
+) : InventoryComponent {
+
+ val panes = mutableObjectListOf()
+ val items = Array>(length) { Array(height) { null } }
+
+ init {
+ require(length > 0) { "Length must be greater than 0" }
+ require(height > 0) { "Height must be greater than 0" }
+ }
+
+ override fun addPane(pane: Pane) {
+ require(pane is AbstractPane) { "Pane must be an instance of AbstractPane" }
+
+ val size = panes.size
+
+ if (size == 0) {
+ panes.add(pane)
+ return
+ }
+
+ val priority = pane.priority
+ var left = 0
+ var right = size - 1
+
+ while (left <= right) {
+ val middle = (left + right) / 2
+ val middlePriority = getPane(middle).priority
+
+ if (middlePriority == priority) {
+ panes.add(middle, pane)
+ return
+ }
+
+ if (middlePriority.isLessThan(priority)) {
+ left = middle + 1
+ } else if (middlePriority.isGreaterThan(priority)) {
+ right = middle - 1
+ }
+ }
+
+ panes.add(right + 1, pane)
+ }
+
+ override fun display(inventory: Inventory, offset: Int) {
+ display()
+
+ placeItems(inventory, offset)
+ }
+
+ override fun display(playerInventory: PlayerInventory, offset: Int) {
+ display()
+
+ placeItems(playerInventory, offset)
+ }
+
+ override fun placeItems(playerInventory: PlayerInventory, offset: Int) {
+ for (x in 0 until length) {
+ for (y in 0 until height) {
+ val slot = if (y == height - 1) {
+ x + offset
+ } else {
+ (y + 1) * length + x + offset
+ }
+
+ playerInventory.setItem(slot, getItem(slot(x, y)))
+ }
+ }
+ }
+
+ override fun placeItems(inventory: Inventory, offset: Int) {
+ for (x in 0 until length) {
+ for (y in 0 until height) {
+ inventory.setItem(y * length + x + offset, getItem(slot(x, y)))
+ }
+ }
+ }
+
+ @OptIn(NmsUseWithCaution::class)
+ fun click(gui: AbstractGui, event: InventoryClickEvent, slot: Int) {
+ val panes = this.panes.freeze()
+
+ for (i in panes.indices.reversed()) {
+ val pane = panes[i]
+ val result = pane.click(
+ gui,
+ component = this,
+ event,
+ slot,
+ paneOffsetX = 0,
+ paneOffsetY = 0,
+ maxLength = length,
+ maxHeight = height
+ )
+
+ if (result) {
+ break
+ }
+ }
+ }
+
+ public override fun clone(): InventoryComponentImpl {
+ val component = InventoryComponentImpl(length, height)
+
+ for (x in 0 until length) {
+ for (y in 0 until height) {
+ val item = getItem(slot(x, y)) ?: continue
+
+ component.setItem(item.clone(), slot(x, y))
+ }
+ }
+
+ for (pane in panes) {
+ component.addPane(pane.clone())
+ }
+
+ return component
+ }
+
+ override fun excludeRows(from: Int, end: Int): InventoryComponentImpl {
+ require(from >= 0) { "From index must be non-negative" }
+ require(end < height) { "End index must be less than height" }
+
+ val newHeight = height - (end - from + 1)
+ val newComponent = InventoryComponentImpl(length, newHeight)
+
+ for (pane in panes) {
+ newComponent.addPane(pane)
+ }
+
+ for (x in 0 until length) {
+ var newY = 0
+
+ for (y in 0 until height) {
+ val item = getItem(slot(x, y))
+
+ if (y >= from && y <= end) {
+ continue
+ }
+
+ if (item != null) {
+ newComponent.setItem(item, slot(x, newY))
+ }
+
+ newY++
+ }
+ }
+
+ return newComponent
+ }
+
+ override fun hasItem(): Boolean {
+ for (x in 0 until length) {
+ for (y in 0 until height) {
+ if (getItem(slot(x, y)) != null) {
+ return true
+ }
+ }
+ }
+
+ return false
+ }
+
+ override fun display() {
+ clearItems()
+
+ for (pane in panes) {
+ if (!pane.visible) {
+ continue
+ }
+
+ pane.display(this, 0, 0, length, height)
+ }
+ }
+
+ override fun hasItem(slot: Slot) = getItem(slot) != null
+
+ override fun getItem(slot: Slot): ItemStack? {
+ val x = slot.getX(length)
+ val y = slot.getY(length)
+
+ require(inBounds(slot)) { "Coordinates ($x, $y) are out of bounds for inventory size $length x $height" }
+
+ return items[x][y]
+ }
+
+ override fun setItem(item: GuiItem, slot: Slot) {
+ val x = slot.getX(length)
+ val y = slot.getY(length)
+
+ require(inBounds(slot)) { "Coordinates ($x, $y) are out of bounds for inventory size $length x $height" }
+
+ items[x][y] = item.clone().apply { applyUuid() }.itemStack
+ }
+
+ override fun setItem(itemStack: ItemStack, slot: Slot) {
+ val x = slot.getX(length)
+ val y = slot.getY(length)
+
+ require(inBounds(slot)) { "Coordinates ($x, $y) are out of bounds for inventory size $length x $height" }
+
+ items[x][y] = itemStack
+ }
+
+ val size get() = length * height
+
+ override fun clearItems() {
+ for (item in items) {
+ item.fill(null)
+ }
+ }
+
+ override fun inBounds(slot: Slot): Boolean {
+ val x = slot.getX(length)
+ val y = slot.getY(length)
+
+ val xBounds = inBounds(0, length - 1, x)
+ val yBounds = inBounds(0, height - 1, y)
+
+ return xBounds && yBounds
+ }
+
+ override fun inBounds(lowerBound: Int, upperBound: Int, value: Int) =
+ value in lowerBound..upperBound
+
+ override fun getPane(index: Int): AbstractPane {
+ if (!inBounds(0, panes.size - 1, index)) {
+ throw IndexOutOfBoundsException("Pane index $index is out of bounds for size ${panes.size}")
+ }
+
+ return panes[index]
+ }
+
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/utils/PlayerInventoryCache.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/utils/PlayerInventoryCache.kt
new file mode 100644
index 00000000..697bc1d7
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/inventory/utils/PlayerInventoryCache.kt
@@ -0,0 +1,96 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.inventory.utils
+
+import dev.slne.surf.surfapi.core.api.util.mutableObject2ObjectMapOf
+import org.bukkit.entity.Player
+import org.bukkit.inventory.ItemStack
+
+class PlayerInventoryCache {
+
+ private val inventories = mutableObject2ObjectMapOf>()
+
+ fun storeAndClear(player: Player) {
+ store(player)
+
+ val inventory = player.inventory
+ for (i in 0 until inventory.size) {
+ inventory.clear(i)
+ }
+ }
+
+ fun restoreAndForget(player: Player) {
+ restore(player)
+ clearCache(player)
+ }
+
+ fun restoreAndForgetAll() {
+ restoreAll()
+ clearCache()
+ }
+
+ fun add(player: Player, item: ItemStack): Int {
+ val items = inventories[player]
+ ?: throw IllegalStateException("The player ${player.uniqueId} does not have a cached inventory")
+
+ var amountPutIn = 0
+
+ for (i in 0 until items.size) {
+ val itemStack = items[i]
+
+ if (itemStack == null) {
+ items[i] = item.clone()
+ items[i]!!.amount = item.amount - amountPutIn
+ amountPutIn = item.amount
+ break
+ }
+
+ if (!itemStack.isSimilar(item)) {
+ continue
+ }
+
+ val additionalAmount = minOf(itemStack.maxStackSize - itemStack.amount, item.amount)
+ itemStack.amount = itemStack.amount + additionalAmount
+ amountPutIn += additionalAmount
+
+ if (amountPutIn == item.amount) {
+ break
+ }
+ }
+
+ return item.amount - amountPutIn
+ }
+
+ fun store(player: Player) {
+ val inventory = player.inventory
+ val items = Array(inventory.size) { null }
+
+ for (i in 0 until inventory.size) {
+ items[i] = inventory.getItem(i)
+ }
+
+ inventories[player] = items
+ }
+
+ private fun restore(player: Player) {
+ val items = inventories[player] ?: return
+ val inventory = player.inventory
+
+ for (i in 0 until items.size) {
+ inventory.setItem(i, items[i])
+ }
+ }
+
+ private fun restoreAll() {
+ inventories.keys.forEach(this::restore)
+ }
+
+ fun contains(player: Player) = inventories.containsKey(player)
+
+ fun clearCache(player: Player) {
+ inventories.remove(player)
+ }
+
+ fun clearCache() {
+ inventories.clear()
+ }
+
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/nms/listener/packets/serverbound/ContainerClickPacketImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/nms/listener/packets/serverbound/ContainerClickPacketImpl.kt
new file mode 100644
index 00000000..0215330b
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/nms/listener/packets/serverbound/ContainerClickPacketImpl.kt
@@ -0,0 +1,35 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.nms.listener.packets.serverbound
+
+import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution
+import dev.slne.surf.surfapi.bukkit.api.nms.listener.packets.serverbound.ContainerClickPacket
+import dev.slne.surf.surfapi.bukkit.server.nms.toWindowClickType
+import dev.slne.surf.surfapi.core.api.util.mutableInt2ObjectMapOf
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap
+import net.minecraft.network.protocol.game.ServerboundContainerClickPacket
+import org.bukkit.entity.Player
+import org.bukkit.inventory.InventoryView
+import org.bukkit.inventory.ItemStack
+
+@NmsUseWithCaution
+class ContainerClickPacketImpl(
+ nmsPacket: ServerboundContainerClickPacket,
+ override val view: InventoryView,
+ override val whoClicked: Player,
+) : NmsServerboundPacketImpl(nmsPacket), ContainerClickPacket {
+ override val containerId get() = nmsPacket.containerId
+ override val stateId get() = nmsPacket.stateId
+ override val slotNumber get() = nmsPacket.slotNum
+ override val buttonNumber get() = nmsPacket.buttonNum
+ override val clickType get() = nmsPacket.clickType.toWindowClickType()
+ override val changedSlots: Int2ObjectMap
+ get() = run {
+ val map = mutableInt2ObjectMapOf()
+
+ nmsPacket.changedSlots.forEach { (key, value) ->
+ map[key] = value.bukkitStack
+ }
+
+ map
+ }
+ override val carriedItem get() = nmsPacket.carriedItem.bukkitStack
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/nms/listener/packets/serverbound/ContainerClosePacketImpl.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/nms/listener/packets/serverbound/ContainerClosePacketImpl.kt
new file mode 100644
index 00000000..36c25fc6
--- /dev/null
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/impl/nms/listener/packets/serverbound/ContainerClosePacketImpl.kt
@@ -0,0 +1,12 @@
+package dev.slne.surf.surfapi.bukkit.server.impl.nms.listener.packets.serverbound
+
+import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution
+import dev.slne.surf.surfapi.bukkit.api.nms.listener.packets.serverbound.ContainerClosePacket
+import net.minecraft.network.protocol.game.ServerboundContainerClosePacket
+
+@NmsUseWithCaution
+class ContainerClosePacketImpl(
+ nmsPacket: ServerboundContainerClosePacket,
+) : NmsServerboundPacketImpl(nmsPacket), ContainerClosePacket {
+ override val containerId get() = nmsPacket.containerId
+}
\ No newline at end of file
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/listener/ListenerManager.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/listener/ListenerManager.kt
index 0b90d0d7..6d8b98d0 100644
--- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/listener/ListenerManager.kt
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/listener/ListenerManager.kt
@@ -1,5 +1,7 @@
package dev.slne.surf.surfapi.bukkit.server.listener
+import dev.slne.surf.surfapi.bukkit.api.event.register
+import dev.slne.surf.surfapi.bukkit.server.impl.inventory.listener.GuiListener
import dev.slne.surf.surfapi.bukkit.server.plugin
import org.bukkit.Bukkit
@@ -9,6 +11,8 @@ object ListenerManager {
*/
fun registerListeners() {
Bukkit.getMessenger().registerOutgoingPluginChannel(plugin, "BungeeCord")
+
+ GuiListener(plugin).register()
}
/**
diff --git a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/nms/nms-extensions.kt b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/nms/nms-extensions.kt
index da93a489..36761bc3 100644
--- a/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/nms/nms-extensions.kt
+++ b/surf-api-bukkit/surf-api-bukkit-server/src/main/kotlin/dev/slne/surf/surfapi/bukkit/server/nms/nms-extensions.kt
@@ -3,7 +3,9 @@
package dev.slne.surf.surfapi.bukkit.server.nms
import dev.slne.surf.surfapi.bukkit.api.extensions.server
+import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution
import dev.slne.surf.surfapi.bukkit.api.nms.bridges.packets.entity.SignBlockUpdateSettings
+import dev.slne.surf.surfapi.bukkit.api.nms.listener.packets.serverbound.ContainerClickPacket.WindowClickType
import io.papermc.paper.adventure.PaperAdventure
import io.papermc.paper.math.BlockPosition
import io.papermc.paper.math.FinePosition
@@ -12,6 +14,7 @@ import net.minecraft.core.BlockPos
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.Display
+import net.minecraft.world.inventory.ClickType
import net.minecraft.world.inventory.MenuType
import net.minecraft.world.item.DyeColor
import net.minecraft.world.item.Item
@@ -51,6 +54,7 @@ fun Material.toNmsItem(): Item = CraftMagicNumbers.getItem(this)
fun BlockData.toNms(): NmsBlockState = (this as CraftBlockData).state
fun Vector3f?.toNms(): NmsVector3f? =
if (this == null) null else NmsVector3f(this.x(), this.y(), this.z())
+
fun FinePosition.toNms() = Vec3(x(), y(), z())
fun BlockState.toNms(): NmsBlockState = (this as CraftBlockState).handle
fun Quaternionf?.toNms(): NmsQuaternionf? =
@@ -75,6 +79,18 @@ fun SignBlockUpdateSettings.SignText.toNms(): SignText {
return SignText(lines, lines, DyeColor.BLACK, false)
}
+@OptIn(NmsUseWithCaution::class)
+fun ClickType.toWindowClickType() = when (this) {
+ ClickType.PICKUP -> WindowClickType.PICKUP
+ ClickType.QUICK_MOVE -> WindowClickType.QUICK_MOVE
+ ClickType.SWAP -> WindowClickType.SWAP
+ ClickType.CLONE -> WindowClickType.CLONE
+ ClickType.THROW -> WindowClickType.THROW
+ ClickType.PICKUP_ALL -> WindowClickType.PICKUP_ALL
+ ClickType.QUICK_CRAFT -> WindowClickType.QUICK_CRAFT
+ else -> WindowClickType.UNKNOWN
+}
+
fun InventoryType.toNms(): MenuType<*> = when (this) {
InventoryType.ANVIL -> MenuType.ANVIL
InventoryType.BEACON -> MenuType.BEACON