From 5ce69e6b7acf3ea54c63765eb6dfa8a9755c8217 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Wed, 10 Jun 2026 00:36:18 +0200 Subject: [PATCH 1/6] SWUI V2 Signed-off-by: Chaoscaot --- BauSystem/BauSystem_Main/build.gradle.kts | 1 + .../bausystem/features/TestCommand.java | 38 +++++++++ BauSystem/build.gradle.kts | 2 + KotlinCore/build.gradle.kts | 7 ++ .../src/de/steamwar/kotlin/ui/Observer.kt | 63 ++++++++++++++ .../de/steamwar/kotlin/ui/RenderBoundary.kt | 40 +++++++++ .../src/de/steamwar/kotlin/ui/RenderMarker.kt | 23 +++++ .../src/de/steamwar/kotlin/ui/TestInv.kt | 49 +++++++++++ .../src/de/steamwar/kotlin/ui/UIInventory.kt | 53 ++++++++++++ .../src/de/steamwar/kotlin/ui/UIWindow.kt | 83 +++++++++++++++++++ .../kotlin/ui/components/ItemContext.kt | 50 +++++++++++ .../kotlin/ui/context/GroupContext.kt | 53 ++++++++++++ .../kotlin/ui/context/WindowContext.kt | 39 +++++++++ buildSrc/src/steamwar.devserver.gradle | 1 - 14 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/TestCommand.java create mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt create mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/RenderBoundary.kt create mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/RenderMarker.kt create mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt create mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt create mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt create mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt create mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt create mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt diff --git a/BauSystem/BauSystem_Main/build.gradle.kts b/BauSystem/BauSystem_Main/build.gradle.kts index f51b33cd..6d594c79 100644 --- a/BauSystem/BauSystem_Main/build.gradle.kts +++ b/BauSystem/BauSystem_Main/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { compileOnly(libs.classindex) annotationProcessor(libs.classindex) compileOnly(project(":SpigotCore", "default")) + compileOnly(project(":KotlinCore", "default")) compileOnly(libs.axiom) compileOnly(libs.authlib) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/TestCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/TestCommand.java new file mode 100644 index 00000000..2aa35725 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/TestCommand.java @@ -0,0 +1,38 @@ +/* + * 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 . + */ + +package de.steamwar.bausystem.features; + +import de.steamwar.command.SWCommand; +import de.steamwar.kotlin.ui.TestInv; +import de.steamwar.linkage.Linked; +import org.bukkit.entity.Player; + +@Linked +public class TestCommand extends SWCommand { + public TestCommand() { + super("supertestcommand"); + } + + @Register + public void test(Player player) { + TestInv inv = new TestInv(player); + inv.open(); + } +} diff --git a/BauSystem/build.gradle.kts b/BauSystem/build.gradle.kts index de9fe1b8..f2843bcf 100644 --- a/BauSystem/build.gradle.kts +++ b/BauSystem/build.gradle.kts @@ -37,5 +37,7 @@ tasks.register("DevBau21") { dependsOn(":SpigotCore:shadowJar") dependsOn(":BauSystem:shadowJar") dependsOn(":SchematicSystem:shadowJar") + dependsOn(":KotlinCore:shadowJar") template = "Bau21" + worldName = "73" } diff --git a/KotlinCore/build.gradle.kts b/KotlinCore/build.gradle.kts index 6c56593d..8467d3ac 100644 --- a/KotlinCore/build.gradle.kts +++ b/KotlinCore/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + /* * This file is a part of the SteamWar software. * @@ -41,3 +43,8 @@ dependencies { implementation(libs.mysql) implementation("org.slf4j:slf4j-simple:2.0.17") } +val compileKotlin: KotlinCompile by tasks + +compileKotlin.compilerOptions { + freeCompilerArgs.set(listOf("-XXLanguage:+ContextParameters")) +} \ No newline at end of file diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt b/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt new file mode 100644 index 00000000..569aad9b --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt @@ -0,0 +1,63 @@ +/* + * 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 . + */ + +package de.steamwar.kotlin.ui + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class Observer(private var value: T): ReadWriteProperty { + private val listeners = mutableSetOf() + + fun removeListener(render: RenderBoundary) { + listeners.remove(render) + } + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return value + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + this.value = value + notifyListeners() + } + + fun set(value: T) { + this.value = value + notifyListeners() + } + + fun get() = value + + fun update(update: (T) -> T) { + this.value = update(value) + notifyListeners() + } + + private fun notifyListeners() { + listeners.forEach { it.update() } + } + + context(render: RenderBoundary) + fun listen(): Observer { + listeners.add(render) + render.observers.add(this) + return this + } +} diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/RenderBoundary.kt b/KotlinCore/src/de/steamwar/kotlin/ui/RenderBoundary.kt new file mode 100644 index 00000000..5074dad5 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/RenderBoundary.kt @@ -0,0 +1,40 @@ +/* + * 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 . + */ + +package de.steamwar.kotlin.ui + +abstract class RenderBoundary(private val parent: RenderBoundary?) { + val observers = mutableSetOf>() + + fun update() { + render() + updateInv() + } + + protected open fun updateInv() { + parent?.updateInv() + } + + abstract fun render() + + open fun destroy() { + observers.forEach { it.removeListener(this) } + observers.clear() + } +} \ No newline at end of file diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/RenderMarker.kt b/KotlinCore/src/de/steamwar/kotlin/ui/RenderMarker.kt new file mode 100644 index 00000000..be7aaaed --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/RenderMarker.kt @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +package de.steamwar.kotlin.ui + +@DslMarker +annotation class RenderMarker() diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt b/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt new file mode 100644 index 00000000..14a0c959 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt @@ -0,0 +1,49 @@ +/* + * 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 . + */ + +package de.steamwar.kotlin.ui + +import de.steamwar.kotlin.ui.components.item +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack + +class TestInv(player: Player): UIInventory(player) { + + override fun render() { + inventory(3, title = "TEST_TITLE") { + group { + item { + var test by Test.test.listen() + x = 1 + y = 1 + item = ItemStack.of(Material.STONE, test) + + onClick { + test++ + } + } + } + } + } +} + +object Test { + val test = Observer(1) +} diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt b/KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt new file mode 100644 index 00000000..a8fbc09b --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt @@ -0,0 +1,53 @@ +/* + * 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 . + */ + +package de.steamwar.kotlin.ui + +import de.steamwar.kotlin.KotlinCore +import de.steamwar.kotlin.ui.context.WindowContext +import net.kyori.adventure.text.event.ClickEvent +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.event.inventory.InventoryType + +abstract class UIInventory(val player: Player) { + lateinit var window: UIWindow + + abstract fun render() + + fun open() { + if (!::window.isInitialized) { + render() + } + + window.open() + } + + protected fun inventory(size: Int, title: String, init: WindowContext.() -> Unit) { + window = UIWindow(size, title, player, init) + } + + protected fun inventory(type: InventoryType, title: String, init: WindowContext.() -> Unit) { + window = UIWindow(type, title, player, init) + } +} \ No newline at end of file diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt b/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt new file mode 100644 index 00000000..cf28f4cb --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt @@ -0,0 +1,83 @@ +/* + * 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 . + */ + +package de.steamwar.kotlin.ui + +import de.steamwar.kotlin.KotlinCore +import de.steamwar.kotlin.ui.context.WindowContext +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.event.inventory.InventoryType +import org.bukkit.inventory.Inventory + +class UIWindow(val bukkitInv: Inventory, val player: Player, val render: WindowContext.() -> Unit): Listener { + val onClicks = mutableMapOf Unit>() + private var updating = false + + constructor(size: Int, title: String, player: Player, render: WindowContext.() -> Unit): this( + Bukkit.createInventory(null, size * 9, Component.translatable(title)), player, render + ) + + constructor(type: InventoryType, title: String, player: Player, render: WindowContext.() -> Unit): this( + Bukkit.createInventory(null, type, Component.translatable(title)), player, render + ) + + fun open() { + render(getContext()) + openInv() + } + + fun openInv() { + updating = true + 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() { + if (updating) return + + onClicks.clear() + } +} \ No newline at end of file diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt b/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt new file mode 100644 index 00000000..3a0407a1 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt @@ -0,0 +1,50 @@ +/* + * 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 . + */ + +package de.steamwar.kotlin.ui.components + +import de.steamwar.kotlin.ui.RenderBoundary +import de.steamwar.kotlin.ui.RenderMarker +import de.steamwar.kotlin.ui.context.GroupContext +import org.bukkit.Material +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.ItemStack + +@RenderMarker +class ItemContext(val parent: GroupContext, val renderFunc: ItemContext.() -> Unit): RenderBoundary(parent) { + override fun render() { + renderFunc(this) + parent.renderItem(x, y, item, onClick) + } + + init { + render() + } + + var item: ItemStack = ItemStack.of(Material.AIR) + var onClick: (event: InventoryClickEvent) -> Unit = {} + var x: Int = 0 + var y: Int = 0 + + fun onClick(func: (event: InventoryClickEvent) -> Unit) { + onClick = func + } +} + +fun GroupContext.item(init: ItemContext.() -> Unit) = children.add(ItemContext(this, init)) diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt b/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt new file mode 100644 index 00000000..a923a520 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt @@ -0,0 +1,53 @@ +/* + * 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 . + */ + +package de.steamwar.kotlin.ui.context + +import de.steamwar.kotlin.ui.RenderBoundary +import de.steamwar.kotlin.ui.RenderMarker +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.ItemStack + +@RenderMarker +open class GroupContext(val parent: GroupContext?, val init: GroupContext.() -> Unit): RenderBoundary(parent) { + val children = mutableListOf() + + override fun destroy() { + children.forEach { it.destroy() } + super.destroy() + } + + init { + init(this) + } + + open fun renderItem(x: Int, y: Int, item: ItemStack, onClick: (event: InventoryClickEvent) -> Unit) { + parent?.renderItem(x, y, item, onClick) + } + + fun group(init: GroupContext.() -> Unit) { + children.add(GroupContext(this, init)) + } + + override fun render() { + children.forEach { it.destroy() } + children.clear() + init(this) + } +} \ No newline at end of file diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt b/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt new file mode 100644 index 00000000..d77b1ef2 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt @@ -0,0 +1,39 @@ +/* + * 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 . + */ + +package de.steamwar.kotlin.ui.context + +import de.steamwar.kotlin.ui.RenderMarker +import de.steamwar.kotlin.ui.UIWindow +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.ItemStack + +@RenderMarker +class WindowContext(val window: UIWindow): GroupContext(null, {}) { + override fun renderItem(x: Int, y: Int, item: ItemStack, onClick: (event: InventoryClickEvent) -> Unit) { + window.bukkitInv.setItem(x + y * 9, item) + window.onClicks[x + y * 9] = onClick + } + + override fun updateInv() = window.openInv() + + fun outsideClick(click: (event: InventoryClickEvent) -> Unit) { + window.onClicks[-999] = click + } +} \ No newline at end of file diff --git a/buildSrc/src/steamwar.devserver.gradle b/buildSrc/src/steamwar.devserver.gradle index eea3fdca..8de7abf0 100644 --- a/buildSrc/src/steamwar.devserver.gradle +++ b/buildSrc/src/steamwar.devserver.gradle @@ -27,7 +27,6 @@ plugins { class DevServer extends DefaultTask { @Input - @Optional boolean debug = false @Input From 5f7ab3cd7bd4930ca97f13a1d5ad8d329660176c Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Wed, 10 Jun 2026 09:26:04 +0200 Subject: [PATCH 2/6] Updates Signed-off-by: Chaoscaot --- .../src/de/steamwar/kotlin/ui/Observer.kt | 2 +- .../de/steamwar/kotlin/ui/RenderBoundary.kt | 11 +-- .../src/de/steamwar/kotlin/ui/TestInv.kt | 3 +- .../src/de/steamwar/kotlin/ui/UIWindow.kt | 78 ++++++++++--------- .../kotlin/ui/components/ItemContext.kt | 8 +- .../kotlin/ui/context/GroupContext.kt | 13 +++- .../kotlin/ui/context/RenderParent.kt | 29 +++++++ .../kotlin/ui/context/WindowContext.kt | 8 +- .../src/de/steamwar/kotlin/util/ItemUtils.kt | 52 +++++++++++++ 9 files changed, 153 insertions(+), 51 deletions(-) create mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/context/RenderParent.kt create mode 100644 KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt b/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt index 569aad9b..a5987273 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt @@ -51,7 +51,7 @@ class Observer(private var value: T): ReadWriteProperty { } private fun notifyListeners() { - listeners.forEach { it.update() } + listeners.forEach { it.render() } } context(render: RenderBoundary) diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/RenderBoundary.kt b/KotlinCore/src/de/steamwar/kotlin/ui/RenderBoundary.kt index 5074dad5..04b0b93d 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/RenderBoundary.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/RenderBoundary.kt @@ -19,18 +19,9 @@ package de.steamwar.kotlin.ui -abstract class RenderBoundary(private val parent: RenderBoundary?) { +abstract class RenderBoundary { val observers = mutableSetOf>() - fun update() { - render() - updateInv() - } - - protected open fun updateInv() { - parent?.updateInv() - } - abstract fun render() open fun destroy() { diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt b/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt index 14a0c959..10738d62 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt @@ -31,7 +31,8 @@ class TestInv(player: Player): UIInventory(player) { group { item { var test by Test.test.listen() - x = 1 + + x = test % 9 y = 1 item = ItemStack.of(Material.STONE, test) diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt b/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt index cf28f4cb..deaacb8d 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt @@ -26,58 +26,66 @@ import org.bukkit.Bukkit import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.Listener +import org.bukkit.event.inventory.ClickType import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.InventoryCloseEvent import org.bukkit.event.inventory.InventoryType 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 Unit>() 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( - Bukkit.createInventory(null, size * 9, Component.translatable(title)), player, render - ) + constructor(size: Int, title: String, player: Player, render: WindowContext.() -> Unit): this(player, render) { + bukkitInv = KotlinCore.plugin.server.createInventory(this, size * 9, Component.translatable(title)) + } - constructor(type: InventoryType, title: String, player: Player, render: WindowContext.() -> Unit): this( - Bukkit.createInventory(null, type, Component.translatable(title)), player, render - ) + constructor(type: InventoryType, title: String, player: Player, render: WindowContext.() -> Unit): this(player, render) { + bukkitInv = KotlinCore.plugin.server.createInventory(this, type, Component.translatable(title)); + } fun open() { - render(getContext()) - openInv() - } - - fun openInv() { - updating = true + render(context) 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() { if (updating) return 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() + } + } } } \ No newline at end of file diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt b/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt index 3a0407a1..d9730802 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt @@ -22,15 +22,21 @@ package de.steamwar.kotlin.ui.components import de.steamwar.kotlin.ui.RenderBoundary import de.steamwar.kotlin.ui.RenderMarker import de.steamwar.kotlin.ui.context.GroupContext +import de.steamwar.kotlin.ui.context.RenderParent import org.bukkit.Material import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.inventory.ItemStack @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() { + val oldX = x + val oldY = y renderFunc(this) parent.renderItem(x, y, item, onClick) + if (oldX != x || oldY != y) { + parent.resetSlot(oldX, oldY) + } } init { diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt b/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt index a923a520..b0f77d86 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt @@ -25,8 +25,9 @@ import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.inventory.ItemStack @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() + val updatedSlots = mutableSetOf>() override fun destroy() { children.forEach { it.destroy() } @@ -37,10 +38,15 @@ open class GroupContext(val parent: GroupContext?, val init: GroupContext.() -> 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) } + override fun resetSlot(x: Int, y: Int) { + parent?.resetSlot(x, y) + } + fun group(init: GroupContext.() -> Unit) { children.add(GroupContext(this, init)) } @@ -48,6 +54,9 @@ open class GroupContext(val parent: GroupContext?, val init: GroupContext.() -> override fun render() { children.forEach { it.destroy() } children.clear() + val oldUpdatedSlots = updatedSlots.toList() + updatedSlots.clear() init(this) + (oldUpdatedSlots - updatedSlots).forEach { resetSlot(it.first, it.second) } } } \ No newline at end of file diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/context/RenderParent.kt b/KotlinCore/src/de/steamwar/kotlin/ui/context/RenderParent.kt new file mode 100644 index 00000000..ad872cf0 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/context/RenderParent.kt @@ -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 . + */ + +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) +} \ No newline at end of file diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt b/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt index d77b1ef2..fa13e683 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt @@ -21,6 +21,7 @@ package de.steamwar.kotlin.ui.context import de.steamwar.kotlin.ui.RenderMarker import de.steamwar.kotlin.ui.UIWindow +import org.bukkit.Material import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.inventory.ItemStack @@ -31,9 +32,14 @@ class WindowContext(val window: UIWindow): GroupContext(null, {}) { 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) { window.onClicks[-999] = click } + + var skipDoubleClick = true } \ No newline at end of file diff --git a/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt b/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt new file mode 100644 index 00000000..111b0ea1 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt @@ -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 . + */ + +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) = 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) } } + + From a6681ffaf636c2b0bdaf7dff8fca804008b5cbaa Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Wed, 10 Jun 2026 16:07:37 +0200 Subject: [PATCH 3/6] Add Top-Level State and more Utils Signed-off-by: Chaoscaot --- .../src/de/steamwar/kotlin/ui/TestInv.kt | 2 +- .../src/de/steamwar/kotlin/ui/UIInventory.kt | 26 +++++++++---------- .../src/de/steamwar/kotlin/ui/UIWindow.kt | 1 + .../src/de/steamwar/kotlin/util/ItemUtils.kt | 2 +- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt b/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt index 10738d62..f4e12f26 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt @@ -26,7 +26,7 @@ import org.bukkit.inventory.ItemStack class TestInv(player: Player): UIInventory(player) { - override fun render() { + override fun view() { inventory(3, title = "TEST_TITLE") { group { item { diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt b/KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt index a8fbc09b..ba7493ed 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt @@ -19,28 +19,28 @@ package de.steamwar.kotlin.ui -import de.steamwar.kotlin.KotlinCore import de.steamwar.kotlin.ui.context.WindowContext -import net.kyori.adventure.text.event.ClickEvent -import org.bukkit.Bukkit import org.bukkit.entity.Player -import org.bukkit.event.EventHandler -import org.bukkit.event.Listener -import org.bukkit.event.inventory.InventoryClickEvent -import org.bukkit.event.inventory.InventoryCloseEvent import org.bukkit.event.inventory.InventoryType -abstract class UIInventory(val player: Player) { - lateinit var window: UIWindow +abstract class UIInventory(val player: Player): RenderBoundary() { + var window: UIWindow? = null - abstract fun render() + abstract fun view() fun open() { - if (!::window.isInitialized) { - render() + if (window == null) { + view() + assert(window != null) { "View method must create a inventory" } } - window.open() + window!!.open() + } + + override fun render() { + window?.onClose() + window = null + open() } protected fun inventory(size: Int, title: String, init: WindowContext.() -> Unit) { diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt b/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt index deaacb8d..e9398689 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt @@ -32,6 +32,7 @@ import org.bukkit.event.inventory.InventoryCloseEvent import org.bukkit.event.inventory.InventoryType import org.bukkit.inventory.Inventory import org.bukkit.inventory.InventoryHolder +import org.bukkit.inventory.InventoryView class UIWindow(val player: Player, val render: WindowContext.() -> Unit): InventoryHolder { val onClicks = mutableMapOf Unit>() diff --git a/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt b/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt index 111b0ea1..d504c679 100644 --- a/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt +++ b/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt @@ -32,7 +32,7 @@ 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) = apply { itemMeta = itemMeta.also { it.lore(lore) } } +fun ItemStack.lored(lore: List) = apply { itemMeta = itemMeta.also { it.lore(lore) } } fun String.asMaterial(): Material = Material.matchMaterial(this) ?: Material.BARRIER From 8050f514241f2e7d33e1dc1cf092affa5e7ac59a Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Wed, 10 Jun 2026 21:43:02 +0200 Subject: [PATCH 4/6] Cleanup for Merge Signed-off-by: Chaoscaot --- .../bausystem/features/TestCommand.java | 38 -------------- BauSystem/build.gradle.kts | 1 - .../src/de/steamwar/kotlin/ui/TestInv.kt | 50 ------------------- .../src/de/steamwar/kotlin/ui/UIWindow.kt | 10 ++-- .../src/de/steamwar/kotlin/util/ItemUtils.kt | 2 - 5 files changed, 4 insertions(+), 97 deletions(-) delete mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/TestCommand.java delete mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/TestCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/TestCommand.java deleted file mode 100644 index 2aa35725..00000000 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/TestCommand.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 . - */ - -package de.steamwar.bausystem.features; - -import de.steamwar.command.SWCommand; -import de.steamwar.kotlin.ui.TestInv; -import de.steamwar.linkage.Linked; -import org.bukkit.entity.Player; - -@Linked -public class TestCommand extends SWCommand { - public TestCommand() { - super("supertestcommand"); - } - - @Register - public void test(Player player) { - TestInv inv = new TestInv(player); - inv.open(); - } -} diff --git a/BauSystem/build.gradle.kts b/BauSystem/build.gradle.kts index f2843bcf..6a6bfac5 100644 --- a/BauSystem/build.gradle.kts +++ b/BauSystem/build.gradle.kts @@ -39,5 +39,4 @@ tasks.register("DevBau21") { dependsOn(":SchematicSystem:shadowJar") dependsOn(":KotlinCore:shadowJar") template = "Bau21" - worldName = "73" } diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt b/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt deleted file mode 100644 index f4e12f26..00000000 --- a/KotlinCore/src/de/steamwar/kotlin/ui/TestInv.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 . - */ - -package de.steamwar.kotlin.ui - -import de.steamwar.kotlin.ui.components.item -import org.bukkit.Material -import org.bukkit.entity.Player -import org.bukkit.inventory.ItemStack - -class TestInv(player: Player): UIInventory(player) { - - override fun view() { - inventory(3, title = "TEST_TITLE") { - group { - item { - var test by Test.test.listen() - - x = test % 9 - y = 1 - item = ItemStack.of(Material.STONE, test) - - onClick { - test++ - } - } - } - } - } -} - -object Test { - val test = Observer(1) -} diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt b/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt index e9398689..87eead32 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt @@ -70,9 +70,8 @@ class UIWindow(val player: Player, val render: WindowContext.() -> Unit): Invent @EventHandler fun onInventoryClick(event: InventoryClickEvent) { - if (event.inventory.holder is UIWindow) { - val window = event.inventory.holder as UIWindow - + val window = event.inventory.holder + if (window is UIWindow) { event.isCancelled = true if (window.context.skipDoubleClick && event.click == ClickType.DOUBLE_CLICK) return @@ -82,9 +81,8 @@ class UIWindow(val player: Player, val render: WindowContext.() -> Unit): Invent @EventHandler fun onInventoryClose(event: InventoryCloseEvent) { - if (event.inventory.holder is UIWindow) { - val window = event.inventory.holder as UIWindow - + val window = event.inventory.holder + if (window is UIWindow) { window.onClose() } } diff --git a/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt b/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt index d504c679..7f709c64 100644 --- a/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt +++ b/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.kt @@ -48,5 +48,3 @@ fun ItemStack.hideAttributes() = apply { itemMeta = itemMeta.also { it.addItemFl 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) } } - - From 70c8678c60bf62290fbf94b8288bb9f0556e7f5e Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Fri, 12 Jun 2026 12:00:04 +0200 Subject: [PATCH 5/6] Refactor rendering system: Introduced `RenderObject` abstraction, replaced `RenderBoundary` with `ObserverListener`, and updated slot calculation for inventory types. Signed-off-by: Chaoscaot --- .../src/de/steamwar/kotlin/ui/Observer.kt | 49 +++++++++++++------ ...{RenderBoundary.kt => ObserverListener.kt} | 6 +-- .../src/de/steamwar/kotlin/ui/RenderObject.kt | 26 ++++++++++ .../src/de/steamwar/kotlin/ui/UIInventory.kt | 4 +- .../src/de/steamwar/kotlin/ui/UIWindow.kt | 3 +- .../kotlin/ui/components/ItemContext.kt | 19 +++++-- .../kotlin/ui/context/GroupContext.kt | 9 ++-- .../kotlin/ui/context/WindowContext.kt | 17 +++++-- 8 files changed, 100 insertions(+), 33 deletions(-) rename KotlinCore/src/de/steamwar/kotlin/ui/{RenderBoundary.kt => ObserverListener.kt} (89%) create mode 100644 KotlinCore/src/de/steamwar/kotlin/ui/RenderObject.kt diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt b/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt index a5987273..8ecb1295 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt @@ -19,16 +19,46 @@ package de.steamwar.kotlin.ui +import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty -class Observer(private var value: T): ReadWriteProperty { - private val listeners = mutableSetOf() +abstract class Observable { + private val listeners = mutableSetOf() - fun removeListener(render: RenderBoundary) { + fun removeListener(render: ObserverListener) { listeners.remove(render) } + internal fun notifyListeners() { + listeners.forEach { it.update() } + } + + context(render: ObserverListener) + fun listen(): Observable { + listeners.add(render) + render.observers.add(this) + return this + } + + fun listen(callback: (T) -> Unit): () -> Unit { + val listener = object: ObserverListener() { + override fun update() = callback(get()) + } + listeners.add(listener) + return { removeListener(listener) } + } + + abstract fun get(): T + + fun map(mapper: (T) -> R): ReadOnlyProperty = Delegate(mapper(get())) + + class Delegate(val value: T): ReadOnlyProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T = value + } +} + +class Observer(private var value: T): ReadWriteProperty, Observable() { override fun getValue(thisRef: Any?, property: KProperty<*>): T { return value } @@ -43,21 +73,10 @@ class Observer(private var value: T): ReadWriteProperty { notifyListeners() } - fun get() = value + override fun get() = value fun update(update: (T) -> T) { this.value = update(value) notifyListeners() } - - private fun notifyListeners() { - listeners.forEach { it.render() } - } - - context(render: RenderBoundary) - fun listen(): Observer { - listeners.add(render) - render.observers.add(this) - return this - } } diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/RenderBoundary.kt b/KotlinCore/src/de/steamwar/kotlin/ui/ObserverListener.kt similarity index 89% rename from KotlinCore/src/de/steamwar/kotlin/ui/RenderBoundary.kt rename to KotlinCore/src/de/steamwar/kotlin/ui/ObserverListener.kt index 04b0b93d..d407f08c 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/RenderBoundary.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/ObserverListener.kt @@ -19,10 +19,10 @@ package de.steamwar.kotlin.ui -abstract class RenderBoundary { - val observers = mutableSetOf>() +abstract class ObserverListener { + val observers = mutableSetOf>() - abstract fun render() + abstract fun update() open fun destroy() { observers.forEach { it.removeListener(this) } diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/RenderObject.kt b/KotlinCore/src/de/steamwar/kotlin/ui/RenderObject.kt new file mode 100644 index 00000000..3fe4d059 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/RenderObject.kt @@ -0,0 +1,26 @@ +/* + * 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 . + */ + +package de.steamwar.kotlin.ui + +interface RenderObject { + fun destroy() + + fun render() +} \ No newline at end of file diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt b/KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt index ba7493ed..c9412434 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/UIInventory.kt @@ -23,7 +23,7 @@ import de.steamwar.kotlin.ui.context.WindowContext import org.bukkit.entity.Player import org.bukkit.event.inventory.InventoryType -abstract class UIInventory(val player: Player): RenderBoundary() { +abstract class UIInventory(val player: Player): ObserverListener() { var window: UIWindow? = null abstract fun view() @@ -37,7 +37,7 @@ abstract class UIInventory(val player: Player): RenderBoundary() { window!!.open() } - override fun render() { + fun render() { window?.onClose() window = null open() diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt b/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt index 87eead32..e4e5cf51 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt @@ -46,7 +46,8 @@ class UIWindow(val player: Player, val render: WindowContext.() -> Unit): Invent } constructor(type: InventoryType, title: String, player: Player, render: WindowContext.() -> Unit): this(player, render) { - bukkitInv = KotlinCore.plugin.server.createInventory(this, type, Component.translatable(title)); + assert(type != InventoryType.CHEST) { "Chest inventories should use the constructor with size" } + bukkitInv = KotlinCore.plugin.server.createInventory(this, type, Component.translatable(title)) } fun open() { diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt b/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt index d9730802..3609e979 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt @@ -19,8 +19,9 @@ package de.steamwar.kotlin.ui.components -import de.steamwar.kotlin.ui.RenderBoundary +import de.steamwar.kotlin.ui.ObserverListener import de.steamwar.kotlin.ui.RenderMarker +import de.steamwar.kotlin.ui.RenderObject import de.steamwar.kotlin.ui.context.GroupContext import de.steamwar.kotlin.ui.context.RenderParent import org.bukkit.Material @@ -28,17 +29,25 @@ import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.inventory.ItemStack @RenderMarker -class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Unit): RenderBoundary() { - override fun render() { +class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Unit): RenderObject, ObserverListener() { + override fun update() { val oldX = x val oldY = y - renderFunc(this) - parent.renderItem(x, y, item, onClick) + render() if (oldX != x || oldY != y) { parent.resetSlot(oldX, oldY) } } + override fun destroy() { + parent.resetSlot(x, y) + } + + override fun render() { + renderFunc(this) + parent.renderItem(x, y, item, onClick) + } + init { render() } diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt b/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt index b0f77d86..f94d70e0 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt @@ -19,16 +19,19 @@ package de.steamwar.kotlin.ui.context -import de.steamwar.kotlin.ui.RenderBoundary +import de.steamwar.kotlin.ui.ObserverListener import de.steamwar.kotlin.ui.RenderMarker +import de.steamwar.kotlin.ui.RenderObject import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.inventory.ItemStack @RenderMarker -open class GroupContext(val parent: RenderParent?, val init: GroupContext.() -> Unit): RenderBoundary(), RenderParent { - val children = mutableListOf() +open class GroupContext(val parent: RenderParent?, val init: GroupContext.() -> Unit): ObserverListener(), RenderObject, RenderParent { + val children = mutableListOf() val updatedSlots = mutableSetOf>() + override fun update() = render() + override fun destroy() { children.forEach { it.destroy() } super.destroy() diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt b/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt index fa13e683..e191d70d 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt @@ -23,18 +23,27 @@ import de.steamwar.kotlin.ui.RenderMarker import de.steamwar.kotlin.ui.UIWindow import org.bukkit.Material import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryType import org.bukkit.inventory.ItemStack @RenderMarker class WindowContext(val window: UIWindow): GroupContext(null, {}) { override fun renderItem(x: Int, y: Int, item: ItemStack, onClick: (event: InventoryClickEvent) -> Unit) { - window.bukkitInv.setItem(x + y * 9, item) - window.onClicks[x + y * 9] = onClick + val slot = calculateSlot(x, y) + window.bukkitInv.setItem(slot, item) + window.onClicks[slot] = onClick } override fun resetSlot(x: Int, y: Int) { - window.bukkitInv.setItem(x + y * 9, ItemStack.of(Material.AIR)) - window.onClicks.remove(x + y * 9) + val slot = calculateSlot(x, y) + window.bukkitInv.setItem(slot, ItemStack.of(Material.AIR)) + window.onClicks.remove(slot) + } + + private fun calculateSlot(x: Int, y: Int) = when (window.inventory.type) { + InventoryType.DROPPER, InventoryType.DISPENSER -> x + y * 3 + InventoryType.HOPPER -> x + else -> x + y * 9 } fun outsideClick(click: (event: InventoryClickEvent) -> Unit) { From 7af586ec166fc377c9ab4f62d01cb3da9829ca66 Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Wed, 17 Jun 2026 21:48:27 +0200 Subject: [PATCH 6/6] Fix Observer Signed-off-by: Chaoscaot --- .../src/de/steamwar/kotlin/ui/Observer.kt | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt b/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt index 8ecb1295..6990e14a 100644 --- a/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt +++ b/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt @@ -24,9 +24,9 @@ import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty abstract class Observable { - private val listeners = mutableSetOf() + internal val listeners = mutableSetOf() - fun removeListener(render: ObserverListener) { + open fun removeListener(render: ObserverListener) { listeners.remove(render) } @@ -46,15 +46,34 @@ abstract class Observable { override fun update() = callback(get()) } listeners.add(listener) + callback(get()) return { removeListener(listener) } } abstract fun get(): T - fun map(mapper: (T) -> R): ReadOnlyProperty = Delegate(mapper(get())) + fun map(mapper: (T) -> R): Observable = Delegate(mapper, this) - class Delegate(val value: T): ReadOnlyProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): T = value + class Delegate(val mapper: (T) -> R, val parent: Observable): Observable(), ReadOnlyProperty { + var value: R? = null + var unsubParent = parent.listen { + val old = value + value = mapper(it) + if (old != value) { + notifyListeners() + } + } + + override fun removeListener(render: ObserverListener) { + super.removeListener(render) + if (listeners.isEmpty()) { + unsubParent() + } + } + + override fun get(): R = value!! + + override fun getValue(thisRef: Any?, property: KProperty<*>): R = value!! } }