Signed-off-by: Chaoscaot <max@maxsp.de>
This commit is contained in:
2026-06-10 09:26:04 +02:00
parent 5ce69e6b7a
commit 5f7ab3cd7b
9 changed files with 153 additions and 51 deletions
@@ -51,7 +51,7 @@ class Observer<T>(private var value: T): ReadWriteProperty<Any?, T> {
} }
private fun notifyListeners() { private fun notifyListeners() {
listeners.forEach { it.update() } listeners.forEach { it.render() }
} }
context(render: RenderBoundary) context(render: RenderBoundary)
@@ -19,18 +19,9 @@
package de.steamwar.kotlin.ui package de.steamwar.kotlin.ui
abstract class RenderBoundary(private val parent: RenderBoundary?) { abstract class RenderBoundary {
val observers = mutableSetOf<Observer<*>>() val observers = mutableSetOf<Observer<*>>()
fun update() {
render()
updateInv()
}
protected open fun updateInv() {
parent?.updateInv()
}
abstract fun render() abstract fun render()
open fun destroy() { open fun destroy() {
@@ -31,7 +31,8 @@ class TestInv(player: Player): UIInventory(player) {
group { group {
item { item {
var test by Test.test.listen() var test by Test.test.listen()
x = 1
x = test % 9
y = 1 y = 1
item = ItemStack.of(Material.STONE, test) item = ItemStack.of(Material.STONE, test)
@@ -26,58 +26,66 @@ import org.bukkit.Bukkit
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.event.EventHandler import org.bukkit.event.EventHandler
import org.bukkit.event.Listener import org.bukkit.event.Listener
import org.bukkit.event.inventory.ClickType
import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.event.inventory.InventoryCloseEvent import org.bukkit.event.inventory.InventoryCloseEvent
import org.bukkit.event.inventory.InventoryType import org.bukkit.event.inventory.InventoryType
import org.bukkit.inventory.Inventory import org.bukkit.inventory.Inventory
import org.bukkit.inventory.InventoryHolder
class UIWindow(val bukkitInv: Inventory, val player: Player, val render: WindowContext.() -> Unit): Listener { class UIWindow(val player: Player, val render: WindowContext.() -> Unit): InventoryHolder {
val onClicks = mutableMapOf<Int, (event: InventoryClickEvent) -> Unit>() val onClicks = mutableMapOf<Int, (event: InventoryClickEvent) -> Unit>()
private var updating = false private var updating = false
lateinit var bukkitInv: Inventory
private set
val context by lazy { WindowContext(this) }
constructor(size: Int, title: String, player: Player, render: WindowContext.() -> Unit): this( constructor(size: Int, title: String, player: Player, render: WindowContext.() -> Unit): this(player, render) {
Bukkit.createInventory(null, size * 9, Component.translatable(title)), player, render bukkitInv = KotlinCore.plugin.server.createInventory(this, size * 9, Component.translatable(title))
) }
constructor(type: InventoryType, title: String, player: Player, render: WindowContext.() -> Unit): this( constructor(type: InventoryType, title: String, player: Player, render: WindowContext.() -> Unit): this(player, render) {
Bukkit.createInventory(null, type, Component.translatable(title)), player, render bukkitInv = KotlinCore.plugin.server.createInventory(this, type, Component.translatable(title));
) }
fun open() { fun open() {
render(getContext()) render(context)
openInv()
}
fun openInv() {
updating = true
player.openInventory(bukkitInv) player.openInventory(bukkitInv)
Bukkit.getPluginManager().registerEvents(this, KotlinCore.plugin)
updating = false
} }
@EventHandler
fun onInventoryClick(event: InventoryClickEvent) {
if (event.whoClicked != player) return
event.isCancelled = true
onClicks[event.slot]?.invoke(event)
}
@EventHandler
fun onInventoryClose(event: InventoryCloseEvent) {
if (event.player != player) return
onClose()
InventoryClickEvent.getHandlerList().unregister(this)
InventoryCloseEvent.getHandlerList().unregister(this)
}
fun getContext() = WindowContext(this)
fun onClose() { fun onClose() {
if (updating) return if (updating) return
onClicks.clear() onClicks.clear()
context.destroy()
}
override fun getInventory() = bukkitInv
companion object : Listener {
init {
Bukkit.getPluginManager().registerEvents(this, KotlinCore.plugin)
}
@EventHandler
fun onInventoryClick(event: InventoryClickEvent) {
if (event.inventory.holder is UIWindow) {
val window = event.inventory.holder as UIWindow
event.isCancelled = true
if (window.context.skipDoubleClick && event.click == ClickType.DOUBLE_CLICK) return
window.onClicks[event.slot]?.invoke(event)
}
}
@EventHandler
fun onInventoryClose(event: InventoryCloseEvent) {
if (event.inventory.holder is UIWindow) {
val window = event.inventory.holder as UIWindow
window.onClose()
}
}
} }
} }
@@ -22,15 +22,21 @@ package de.steamwar.kotlin.ui.components
import de.steamwar.kotlin.ui.RenderBoundary import de.steamwar.kotlin.ui.RenderBoundary
import de.steamwar.kotlin.ui.RenderMarker import de.steamwar.kotlin.ui.RenderMarker
import de.steamwar.kotlin.ui.context.GroupContext import de.steamwar.kotlin.ui.context.GroupContext
import de.steamwar.kotlin.ui.context.RenderParent
import org.bukkit.Material import org.bukkit.Material
import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
@RenderMarker @RenderMarker
class ItemContext(val parent: GroupContext, val renderFunc: ItemContext.() -> Unit): RenderBoundary(parent) { class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Unit): RenderBoundary() {
override fun render() { override fun render() {
val oldX = x
val oldY = y
renderFunc(this) renderFunc(this)
parent.renderItem(x, y, item, onClick) parent.renderItem(x, y, item, onClick)
if (oldX != x || oldY != y) {
parent.resetSlot(oldX, oldY)
}
} }
init { init {
@@ -25,8 +25,9 @@ import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
@RenderMarker @RenderMarker
open class GroupContext(val parent: GroupContext?, val init: GroupContext.() -> Unit): RenderBoundary(parent) { open class GroupContext(val parent: RenderParent?, val init: GroupContext.() -> Unit): RenderBoundary(), RenderParent {
val children = mutableListOf<RenderBoundary>() val children = mutableListOf<RenderBoundary>()
val updatedSlots = mutableSetOf<Pair<Int, Int>>()
override fun destroy() { override fun destroy() {
children.forEach { it.destroy() } children.forEach { it.destroy() }
@@ -37,10 +38,15 @@ open class GroupContext(val parent: GroupContext?, val init: GroupContext.() ->
init(this) init(this)
} }
open fun renderItem(x: Int, y: Int, item: ItemStack, onClick: (event: InventoryClickEvent) -> Unit) { override fun renderItem(x: Int, y: Int, item: ItemStack, onClick: (event: InventoryClickEvent) -> Unit) {
updatedSlots.add(x to y)
parent?.renderItem(x, y, item, onClick) parent?.renderItem(x, y, item, onClick)
} }
override fun resetSlot(x: Int, y: Int) {
parent?.resetSlot(x, y)
}
fun group(init: GroupContext.() -> Unit) { fun group(init: GroupContext.() -> Unit) {
children.add(GroupContext(this, init)) children.add(GroupContext(this, init))
} }
@@ -48,6 +54,9 @@ open class GroupContext(val parent: GroupContext?, val init: GroupContext.() ->
override fun render() { override fun render() {
children.forEach { it.destroy() } children.forEach { it.destroy() }
children.clear() children.clear()
val oldUpdatedSlots = updatedSlots.toList()
updatedSlots.clear()
init(this) init(this)
(oldUpdatedSlots - updatedSlots).forEach { resetSlot(it.first, it.second) }
} }
} }
@@ -0,0 +1,29 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.kotlin.ui.context
import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.inventory.ItemStack
interface RenderParent {
fun renderItem(x: Int, y: Int, item: ItemStack, onClick: (event: InventoryClickEvent) -> Unit)
fun resetSlot(x: Int, y: Int)
}
@@ -21,6 +21,7 @@ package de.steamwar.kotlin.ui.context
import de.steamwar.kotlin.ui.RenderMarker import de.steamwar.kotlin.ui.RenderMarker
import de.steamwar.kotlin.ui.UIWindow import de.steamwar.kotlin.ui.UIWindow
import org.bukkit.Material
import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
@@ -31,9 +32,14 @@ class WindowContext(val window: UIWindow): GroupContext(null, {}) {
window.onClicks[x + y * 9] = onClick window.onClicks[x + y * 9] = onClick
} }
override fun updateInv() = window.openInv() override fun resetSlot(x: Int, y: Int) {
window.bukkitInv.setItem(x + y * 9, ItemStack.of(Material.AIR))
window.onClicks.remove(x + y * 9)
}
fun outsideClick(click: (event: InventoryClickEvent) -> Unit) { fun outsideClick(click: (event: InventoryClickEvent) -> Unit) {
window.onClicks[-999] = click window.onClicks[-999] = click
} }
var skipDoubleClick = true
} }
@@ -0,0 +1,52 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.kotlin.util
import net.kyori.adventure.text.Component
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.enchantments.Enchantment
import org.bukkit.inventory.ItemFlag
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.SkullMeta
import org.bukkit.inventory.meta.components.CustomModelDataComponent
fun ItemStack.count(count: Int) = apply { amount = count }
fun ItemStack.name(name: Component) = apply { itemMeta = itemMeta.also { it.displayName(name) } }
fun ItemStack.lore(lore: List<Component>) = apply { itemMeta = itemMeta.also { it.lore(lore) } }
fun String.asMaterial(): Material = Material.matchMaterial(this) ?: Material.BARRIER
fun skull(owner: String): ItemStack = ItemStack(Material.PLAYER_HEAD)
.also {
it.editMeta(SkullMeta::class.java) {
it.playerProfile = Bukkit.getOfflinePlayer(owner.trimStart('.')).playerProfile.also { pp -> pp.complete() }
}
}
fun ItemStack.hideAttributes() = apply { itemMeta = itemMeta.also { it.addItemFlags(*ItemFlag.entries.toTypedArray()) } }
fun ItemStack.enchanted() = apply { itemMeta = itemMeta.also { it.addEnchant(Enchantment.UNBREAKING, 10, true) } }
fun ItemStack.customModelData(model: CustomModelDataComponent) = apply { itemMeta = itemMeta.also { it.setCustomModelDataComponent(model) } }