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(project(":SpigotCore"))
|
||||
|
||||
implementation(libs.coroutinesCore)
|
||||
implementation(libs.exposedCore)
|
||||
implementation(libs.exposedDao)
|
||||
implementation(libs.exposedJdbc)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-4
@@ -19,13 +19,23 @@
|
||||
|
||||
package de.steamwar.kotlin.ui
|
||||
|
||||
abstract class ObserverListener {
|
||||
val observers = mutableSetOf<Observable<*>>()
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
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()
|
||||
|
||||
open fun destroy() {
|
||||
observers.forEach { it.removeListener(this) }
|
||||
observers.clear()
|
||||
jobs.values.toList().forEach { it.cancel() }
|
||||
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.event.inventory.InventoryType
|
||||
|
||||
abstract class UIInventory(val player: Player): ObserverListener() {
|
||||
abstract class UIInventory(val player: Player): StateFlowListener() {
|
||||
var window: UIWindow? = null
|
||||
|
||||
abstract fun view()
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
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.RenderObject
|
||||
import de.steamwar.kotlin.ui.context.GroupContext
|
||||
@@ -29,7 +29,7 @@ import org.bukkit.event.inventory.InventoryClickEvent
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
@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() {
|
||||
val oldX = x
|
||||
val oldY = y
|
||||
@@ -40,6 +40,7 @@ class ItemContext(val parent: RenderParent, val renderFunc: ItemContext.() -> Un
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
super.destroy()
|
||||
parent.resetSlot(x, y)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
|
||||
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.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 {
|
||||
open class GroupContext(val parent: RenderParent?, val init: GroupContext.() -> Unit): StateFlowListener(), RenderObject, RenderParent {
|
||||
val children = mutableListOf<RenderObject>()
|
||||
val updatedSlots = mutableSetOf<Pair<Int, Int>>()
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ dependencyResolutionManagement {
|
||||
library("msgpack", "org.msgpack:msgpack-core:0.9.8")
|
||||
|
||||
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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user