forked from SteamWar/SteamWar
Replace Custom Observer with kotlinx StateFlows
Signed-off-by: Chaoscaot <max@maxsp.de>
This commit is contained in:
@@ -36,6 +36,7 @@ dependencies {
|
|||||||
compileOnly(libs.paperapi)
|
compileOnly(libs.paperapi)
|
||||||
compileOnly(project(":SpigotCore"))
|
compileOnly(project(":SpigotCore"))
|
||||||
|
|
||||||
|
implementation(libs.coroutinesCore)
|
||||||
implementation(libs.exposedCore)
|
implementation(libs.exposedCore)
|
||||||
implementation(libs.exposedDao)
|
implementation(libs.exposedDao)
|
||||||
implementation(libs.exposedJdbc)
|
implementation(libs.exposedJdbc)
|
||||||
@@ -47,4 +48,4 @@ val compileKotlin: KotlinCompile by tasks
|
|||||||
|
|
||||||
compileKotlin.compilerOptions {
|
compileKotlin.compilerOptions {
|
||||||
freeCompilerArgs.set(listOf("-XXLanguage:+ContextParameters"))
|
freeCompilerArgs.set(listOf("-XXLanguage:+ContextParameters"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package de.steamwar.kotlin.ui
|
|
||||||
|
|
||||||
import kotlin.properties.ReadOnlyProperty
|
|
||||||
import kotlin.properties.ReadWriteProperty
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
abstract class Observable<T> {
|
|
||||||
internal val listeners = mutableSetOf<ObserverListener>()
|
|
||||||
|
|
||||||
open fun removeListener(render: ObserverListener) {
|
|
||||||
listeners.remove(render)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun notifyListeners() {
|
|
||||||
listeners.forEach { it.update() }
|
|
||||||
}
|
|
||||||
|
|
||||||
context(render: ObserverListener)
|
|
||||||
fun listen(): Observable<T> {
|
|
||||||
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 <R> map(mapper: (T) -> R): Observable<R> = Delegate(mapper, this)
|
|
||||||
|
|
||||||
class Delegate<T, R>(val mapper: (T) -> R, val parent: Observable<T>): Observable<R>(), ReadOnlyProperty<Any?, R> {
|
|
||||||
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<T>(private var value: T): ReadWriteProperty<Any?, T>, Observable<T>() {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
private val uiStateScope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)
|
||||||
|
|
||||||
|
context(render: StateFlowListener)
|
||||||
|
fun <T> StateFlow<T>.listen(): StateFlow<T> {
|
||||||
|
render.track(this) {
|
||||||
|
drop(1).onEach { render.update() }.launchIn(uiStateScope)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
context(render: StateFlowListener)
|
||||||
|
fun <T> MutableStateFlow<T>.listen(): MutableStateFlow<T> {
|
||||||
|
render.track(this) {
|
||||||
|
drop(1).onEach { render.update() }.launchIn(uiStateScope)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> StateFlow<T>.listen(callback: (T) -> Unit): () -> Unit {
|
||||||
|
callback(value)
|
||||||
|
val job = drop(1).onEach { callback(it) }.launchIn(uiStateScope)
|
||||||
|
return { job.cancel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun <T> StateFlow<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
|
||||||
|
|
||||||
|
operator fun <T> MutableStateFlow<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T, R> StateFlow<T>.map(mapper: (T) -> R): StateFlow<R> = MappedStateFlow(this, mapper)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForInheritanceCoroutinesApi::class)
|
||||||
|
private class MappedStateFlow<T, R>(
|
||||||
|
private val parent: StateFlow<T>,
|
||||||
|
private val mapper: (T) -> R,
|
||||||
|
): StateFlow<R> {
|
||||||
|
override val replayCache: List<R>
|
||||||
|
get() = listOf(value)
|
||||||
|
|
||||||
|
override val value: R
|
||||||
|
get() = mapper(parent.value)
|
||||||
|
|
||||||
|
override suspend fun collect(collector: FlowCollector<R>): Nothing {
|
||||||
|
var initialized = false
|
||||||
|
var previous: Any? = null
|
||||||
|
|
||||||
|
parent.collect {
|
||||||
|
val mapped = mapper(it)
|
||||||
|
if (!initialized || previous != mapped) {
|
||||||
|
initialized = true
|
||||||
|
previous = mapped
|
||||||
|
collector.emit(mapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-5
@@ -19,13 +19,23 @@
|
|||||||
|
|
||||||
package de.steamwar.kotlin.ui
|
package de.steamwar.kotlin.ui
|
||||||
|
|
||||||
abstract class ObserverListener {
|
import kotlinx.coroutines.Job
|
||||||
val observers = mutableSetOf<Observable<*>>()
|
|
||||||
|
abstract class StateFlowListener {
|
||||||
|
private val jobs = mutableMapOf<Any, Job>()
|
||||||
|
|
||||||
|
internal fun track(key: Any, createJob: () -> Job) {
|
||||||
|
if (key in jobs) return
|
||||||
|
|
||||||
|
val job = createJob()
|
||||||
|
jobs[key] = job
|
||||||
|
job.invokeOnCompletion { jobs.remove(key) }
|
||||||
|
}
|
||||||
|
|
||||||
abstract fun update()
|
abstract fun update()
|
||||||
|
|
||||||
open fun destroy() {
|
open fun destroy() {
|
||||||
observers.forEach { it.removeListener(this) }
|
jobs.values.toList().forEach { it.cancel() }
|
||||||
observers.clear()
|
jobs.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import de.steamwar.kotlin.ui.components.item
|
||||||
|
import de.steamwar.kotlin.util.count
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.bukkit.Material
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.inventory.ItemStack
|
||||||
|
|
||||||
|
val Counter = MutableStateFlow(0)
|
||||||
|
|
||||||
|
class TestInv(player: Player): UIInventory(player) {
|
||||||
|
override fun view() {
|
||||||
|
inventory(3, "") {
|
||||||
|
item {
|
||||||
|
val amount by Counter.map { it + 1 }.listen()
|
||||||
|
item = ItemStack.of(Material.STONE)
|
||||||
|
.count(amount)
|
||||||
|
x = 0
|
||||||
|
y = 0
|
||||||
|
onClick {
|
||||||
|
Counter.value += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ import de.steamwar.kotlin.ui.context.WindowContext
|
|||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
import org.bukkit.event.inventory.InventoryType
|
import org.bukkit.event.inventory.InventoryType
|
||||||
|
|
||||||
abstract class UIInventory(val player: Player): ObserverListener() {
|
abstract class UIInventory(val player: Player): StateFlowListener() {
|
||||||
var window: UIWindow? = null
|
var window: UIWindow? = null
|
||||||
|
|
||||||
abstract fun view()
|
abstract fun view()
|
||||||
@@ -52,4 +52,4 @@ abstract class UIInventory(val player: Player): ObserverListener() {
|
|||||||
protected fun inventory(type: InventoryType, title: String, init: WindowContext.() -> Unit) {
|
protected fun inventory(type: InventoryType, title: String, init: WindowContext.() -> Unit) {
|
||||||
window = UIWindow(type, title, player, init)
|
window = UIWindow(type, title, player, init)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
package de.steamwar.kotlin.ui.components
|
package de.steamwar.kotlin.ui.components
|
||||||
|
|
||||||
import de.steamwar.kotlin.ui.ObserverListener
|
import de.steamwar.kotlin.ui.StateFlowListener
|
||||||
import de.steamwar.kotlin.ui.RenderMarker
|
import de.steamwar.kotlin.ui.RenderMarker
|
||||||
import de.steamwar.kotlin.ui.RenderObject
|
import de.steamwar.kotlin.ui.RenderObject
|
||||||
import de.steamwar.kotlin.ui.context.GroupContext
|
import de.steamwar.kotlin.ui.context.GroupContext
|
||||||
@@ -29,7 +29,7 @@ import org.bukkit.event.inventory.InventoryClickEvent
|
|||||||
import org.bukkit.inventory.ItemStack
|
import org.bukkit.inventory.ItemStack
|
||||||
|
|
||||||
@RenderMarker
|
@RenderMarker
|
||||||
class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Unit): RenderObject, ObserverListener() {
|
class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Unit): RenderObject, StateFlowListener() {
|
||||||
override fun update() {
|
override fun update() {
|
||||||
val oldX = x
|
val oldX = x
|
||||||
val oldY = y
|
val oldY = y
|
||||||
@@ -40,6 +40,7 @@ class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Un
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
|
super.destroy()
|
||||||
parent.resetSlot(x, y)
|
parent.resetSlot(x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,14 +19,14 @@
|
|||||||
|
|
||||||
package de.steamwar.kotlin.ui.context
|
package de.steamwar.kotlin.ui.context
|
||||||
|
|
||||||
import de.steamwar.kotlin.ui.ObserverListener
|
import de.steamwar.kotlin.ui.StateFlowListener
|
||||||
import de.steamwar.kotlin.ui.RenderMarker
|
import de.steamwar.kotlin.ui.RenderMarker
|
||||||
import de.steamwar.kotlin.ui.RenderObject
|
import de.steamwar.kotlin.ui.RenderObject
|
||||||
import org.bukkit.event.inventory.InventoryClickEvent
|
import org.bukkit.event.inventory.InventoryClickEvent
|
||||||
import org.bukkit.inventory.ItemStack
|
import org.bukkit.inventory.ItemStack
|
||||||
|
|
||||||
@RenderMarker
|
@RenderMarker
|
||||||
open class GroupContext(val parent: RenderParent?, val init: GroupContext.() -> Unit): ObserverListener(), RenderObject, RenderParent {
|
open class GroupContext(val parent: RenderParent?, val init: GroupContext.() -> Unit): StateFlowListener(), RenderObject, RenderParent {
|
||||||
val children = mutableListOf<RenderObject>()
|
val children = mutableListOf<RenderObject>()
|
||||||
val updatedSlots = mutableSetOf<Pair<Int, Int>>()
|
val updatedSlots = mutableSetOf<Pair<Int, Int>>()
|
||||||
|
|
||||||
@@ -62,4 +62,4 @@ open class GroupContext(val parent: RenderParent?, val init: GroupContext.() ->
|
|||||||
init(this)
|
init(this)
|
||||||
(oldUpdatedSlots - updatedSlots).forEach { resetSlot(it.first, it.second) }
|
(oldUpdatedSlots - updatedSlots).forEach { resetSlot(it.first, it.second) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ dependencyResolutionManagement {
|
|||||||
library("msgpack", "org.msgpack:msgpack-core:0.9.8")
|
library("msgpack", "org.msgpack:msgpack-core:0.9.8")
|
||||||
|
|
||||||
library("logback", "ch.qos.logback:logback-classic:1.5.6")
|
library("logback", "ch.qos.logback:logback-classic:1.5.6")
|
||||||
|
library("coroutinesCore", "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||||
|
|
||||||
val ktorVersion = "2.3.12"
|
val ktorVersion = "2.3.12"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user