diff --git a/BauSystem/BauSystem_Main/build.gradle.kts b/BauSystem/BauSystem_Main/build.gradle.kts index 7f5f9b18..c6316f7d 100644 --- a/BauSystem/BauSystem_Main/build.gradle.kts +++ b/BauSystem/BauSystem_Main/build.gradle.kts @@ -35,6 +35,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/build.gradle.kts b/BauSystem/build.gradle.kts index de9fe1b8..6a6bfac5 100644 --- a/BauSystem/build.gradle.kts +++ b/BauSystem/build.gradle.kts @@ -37,5 +37,6 @@ tasks.register("DevBau21") { dependsOn(":SpigotCore:shadowJar") dependsOn(":BauSystem:shadowJar") dependsOn(":SchematicSystem:shadowJar") + dependsOn(":KotlinCore:shadowJar") template = "Bau21" } 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..6990e14a --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/Observer.kt @@ -0,0 +1,101 @@ +/* + * 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.ReadOnlyProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +abstract class Observable { + internal val listeners = mutableSetOf() + + open 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) + callback(get()) + return { removeListener(listener) } + } + + abstract fun get(): T + + fun map(mapper: (T) -> R): Observable = Delegate(mapper, this) + + 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!! + } +} + +class Observer(private var value: T): ReadWriteProperty, Observable() { + 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() + } + + override fun get() = value + + fun update(update: (T) -> T) { + this.value = update(value) + notifyListeners() + } +} diff --git a/KotlinCore/src/de/steamwar/kotlin/ui/ObserverListener.kt b/KotlinCore/src/de/steamwar/kotlin/ui/ObserverListener.kt new file mode 100644 index 00000000..d407f08c --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/ObserverListener.kt @@ -0,0 +1,31 @@ +/* + * 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 ObserverListener { + val observers = mutableSetOf>() + + abstract fun update() + + 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/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 new file mode 100644 index 00000000..c9412434 --- /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.ui.context.WindowContext +import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryType + +abstract class UIInventory(val player: Player): ObserverListener() { + var window: UIWindow? = null + + abstract fun view() + + fun open() { + if (window == null) { + view() + assert(window != null) { "View method must create a inventory" } + } + + window!!.open() + } + + fun render() { + window?.onClose() + window = null + 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..e4e5cf51 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/UIWindow.kt @@ -0,0 +1,91 @@ +/* + * 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.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 +import org.bukkit.inventory.InventoryView + +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(player, render) { + bukkitInv = KotlinCore.plugin.server.createInventory(this, size * 9, Component.translatable(title)) + } + + constructor(type: InventoryType, title: String, player: Player, render: WindowContext.() -> Unit): this(player, render) { + assert(type != InventoryType.CHEST) { "Chest inventories should use the constructor with size" } + bukkitInv = KotlinCore.plugin.server.createInventory(this, type, Component.translatable(title)) + } + + fun open() { + render(context) + player.openInventory(bukkitInv) + } + + 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) { + val window = event.inventory.holder + if (window is 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) { + val window = event.inventory.holder + if (window is 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 new file mode 100644 index 00000000..3609e979 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/components/ItemContext.kt @@ -0,0 +1,65 @@ +/* + * 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.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 +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.ItemStack + +@RenderMarker +class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Unit): RenderObject, ObserverListener() { + override fun update() { + val oldX = x + val oldY = y + 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() + } + + 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..f94d70e0 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/context/GroupContext.kt @@ -0,0 +1,65 @@ +/* + * 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.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): ObserverListener(), RenderObject, RenderParent { + val children = mutableListOf() + val updatedSlots = mutableSetOf>() + + override fun update() = render() + + override fun destroy() { + children.forEach { it.destroy() } + super.destroy() + } + + init { + init(this) + } + + 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)) + } + + 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 new file mode 100644 index 00000000..e191d70d --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/ui/context/WindowContext.kt @@ -0,0 +1,54 @@ +/* + * 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.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) { + val slot = calculateSlot(x, y) + window.bukkitInv.setItem(slot, item) + window.onClicks[slot] = onClick + } + + override fun resetSlot(x: Int, y: Int) { + 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) { + 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..7f709c64 --- /dev/null +++ b/KotlinCore/src/de/steamwar/kotlin/util/ItemUtils.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.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.lored(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) } }