From 132cdccd0a3825142e2fd33391a5337be38e47fa Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Sun, 8 Nov 2020 22:45:37 +0100 Subject: [PATCH] Entities are now shown per area and area selection is now possible. Fixed some bugs. --- .../lib/fileFormats/quest/QuestNpc.kt | 4 +- .../lib/fileFormats/quest/QuestObject.kt | 4 +- .../observable/value/AbstractVal.kt | 11 +- .../observable/value/DelegatingVal.kt | 2 +- .../observable/value/DependentVal.kt | 23 ++- .../observable/value/FlatMappedVal.kt | 16 +- .../phantasmal/observable/value/SimpleVal.kt | 3 +- .../phantasmal/observable/value/StaticVal.kt | 5 +- .../world/phantasmal/observable/value/Val.kt | 6 +- .../observable/value/ValObserver.kt | 9 - .../observable/value/list/AbstractListVal.kt | 135 ++++++++++++++ .../observable/value/list/DependentListVal.kt | 129 +++++++++++++ .../observable/value/list/FoldedVal.kt | 7 +- .../observable/value/list/ListVal.kt | 3 + .../observable/value/list/SimpleListVal.kt | 174 ++---------------- .../observable/value/list/StaticListVal.kt | 7 +- .../value/list/DependentListValTests.kt | 9 + .../phantasmal/web/questEditor/QuestEditor.kt | 2 +- .../QuestEditorToolbarController.kt | 30 +++ .../questEditor/models/QuestEntityModel.kt | 8 + .../questEditor/models/QuestObjectModel.kt | 14 +- .../rendering/EntityMeshManager.kt | 84 +++++---- .../rendering/QuestEditorMeshManager.kt | 6 +- .../questEditor/rendering/QuestMeshManager.kt | 8 +- .../questEditor/stores/QuestEditorStore.kt | 21 ++- ...Toolbar.kt => QuestEditorToolbarWidget.kt} | 15 +- .../world/phantasmal/webui/widgets/Select.kt | 6 +- 27 files changed, 478 insertions(+), 263 deletions(-) delete mode 100644 observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValObserver.kt create mode 100644 observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractListVal.kt create mode 100644 observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/DependentListVal.kt create mode 100644 observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/DependentListValTests.kt rename web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/{QuestEditorToolbar.kt => QuestEditorToolbarWidget.kt} (75%) diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestNpc.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestNpc.kt index 436b3d41..c904f0f0 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestNpc.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestNpc.kt @@ -70,9 +70,9 @@ class QuestNpc( } override var sectionId: Int - get() = data.getUShort(12).toInt() + get() = data.getShort(12).toInt() set(value) { - data.setUShort(12, value.toUShort()) + data.setShort(12, value.toShort()) } override var position: Vec3 diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestObject.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestObject.kt index fb4c1363..144330ab 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestObject.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/quest/QuestObject.kt @@ -20,9 +20,9 @@ class QuestObject(override var areaId: Int, val data: Buffer) : QuestEntity : Val { - protected val observers: MutableList> = mutableListOf() + protected val observers: MutableList> = mutableListOf() final override fun observe(observer: Observer): Disposable = observe(callNow = false, observer) - override fun observe(callNow: Boolean, observer: ValObserver): Disposable { + override fun observe(callNow: Boolean, observer: Observer): Disposable { observers.add(observer) if (callNow) { - observer(ValChangeEvent(value, value)) + observer(ChangeEvent(value)) } return disposable { @@ -22,8 +23,8 @@ abstract class AbstractVal : Val { } } - protected fun emit(oldValue: T) { - val event = ValChangeEvent(value, oldValue) + protected fun emit() { + val event = ChangeEvent(value) observers.forEach { it(event) } } } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DelegatingVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DelegatingVal.kt index 8ccb1dc9..2cd12fdb 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DelegatingVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DelegatingVal.kt @@ -11,7 +11,7 @@ class DelegatingVal( if (value != oldValue) { setter(value) - emit(oldValue) + emit() } } } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt index bd6bef8b..bea9fba1 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/DependentVal.kt @@ -3,6 +3,7 @@ package world.phantasmal.observable.value import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.disposable import world.phantasmal.core.unsafeToNonNull +import world.phantasmal.observable.Observer /** * Starts observing its dependencies when the first observer on this val is registered. Stops @@ -17,19 +18,26 @@ abstract class DependentVal( */ private val dependencyObservers = mutableListOf() + /** + * Set to true right before actual observers are added. + */ + protected var hasObservers = false + protected var _value: T? = null override val value: T get() { - if (hasNoObservers()) { + if (!hasObservers) { _value = computeValue() } return _value.unsafeToNonNull() } - override fun observe(callNow: Boolean, observer: ValObserver): Disposable { - if (hasNoObservers()) { + override fun observe(callNow: Boolean, observer: Observer): Disposable { + if (dependencyObservers.isEmpty()) { + hasObservers = true + dependencies.forEach { dependency -> dependencyObservers.add( dependency.observe { @@ -37,7 +45,7 @@ abstract class DependentVal( _value = computeValue() if (_value != oldValue) { - emit(oldValue.unsafeToNonNull()) + emit() } } ) @@ -52,17 +60,12 @@ abstract class DependentVal( superDisposable.dispose() if (observers.isEmpty()) { + hasObservers = false dependencyObservers.forEach { it.dispose() } dependencyObservers.clear() } } } - protected fun hasObservers(): Boolean = - dependencyObservers.isNotEmpty() - - protected fun hasNoObservers(): Boolean = - dependencyObservers.isEmpty() - protected abstract fun computeValue(): T } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatMappedVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatMappedVal.kt index b3f14d64..e5eff9d7 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatMappedVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/FlatMappedVal.kt @@ -3,6 +3,7 @@ package world.phantasmal.observable.value import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.disposable import world.phantasmal.core.unsafeToNonNull +import world.phantasmal.observable.Observer class FlatMappedVal( dependencies: Iterable>, @@ -13,20 +14,20 @@ class FlatMappedVal( override val value: T get() { - return if (hasNoObservers()) { - super.value - } else { + return if (hasObservers) { computedVal.unsafeToNonNull().value + } else { + super.value } } - override fun observe(callNow: Boolean, observer: ValObserver): Disposable { + override fun observe(callNow: Boolean, observer: Observer): Disposable { val superDisposable = super.observe(callNow, observer) return disposable { superDisposable.dispose() - if (hasNoObservers()) { + if (!hasObservers) { computedValObserver?.dispose() computedValObserver = null computedVal = null @@ -40,11 +41,10 @@ class FlatMappedVal( computedValObserver?.dispose() - if (hasObservers()) { + if (hasObservers) { computedValObserver = computedVal.observe { (value) -> - val oldValue = _value.unsafeToNonNull() _value = value - emit(oldValue) + emit() } } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/SimpleVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/SimpleVal.kt index 5cda0466..3e0a2929 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/SimpleVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/SimpleVal.kt @@ -4,9 +4,8 @@ class SimpleVal(value: T) : AbstractVal(), MutableVal { override var value: T = value set(value) { if (value != field) { - val oldValue = field field = value - emit(oldValue) + emit() } } } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/StaticVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/StaticVal.kt index 629e86f6..7558f19e 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/StaticVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/StaticVal.kt @@ -2,12 +2,13 @@ package world.phantasmal.observable.value import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.stubDisposable +import world.phantasmal.observable.ChangeEvent import world.phantasmal.observable.Observer class StaticVal(override val value: T) : Val { - override fun observe(callNow: Boolean, observer: ValObserver): Disposable { + override fun observe(callNow: Boolean, observer: Observer): Disposable { if (callNow) { - observer(ValChangeEvent(value, value)) + observer(ChangeEvent(value)) } return stubDisposable() diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt index a1800a59..6d965fa2 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/Val.kt @@ -2,6 +2,7 @@ package world.phantasmal.observable.value import world.phantasmal.core.disposable.Disposable import world.phantasmal.observable.Observable +import world.phantasmal.observable.Observer import kotlin.reflect.KProperty /** @@ -15,7 +16,7 @@ interface Val : Observable { /** * @param callNow Call [observer] immediately with the current [mutableVal]. */ - fun observe(callNow: Boolean = false, observer: ValObserver): Disposable + fun observe(callNow: Boolean = false, observer: Observer): Disposable fun map(transform: (T) -> R): Val = MappedVal(listOf(this)) { transform(value) } @@ -23,6 +24,9 @@ interface Val : Observable { fun map(v2: Val, transform: (T, T2) -> R): Val = MappedVal(listOf(this, v2)) { transform(value, v2.value) } + fun map(v2: Val, v3: Val, transform: (T, T2, T3) -> R): Val = + MappedVal(listOf(this, v2, v3)) { transform(value, v2.value, v3.value) } + fun flatMap(transform: (T) -> Val): Val = FlatMappedVal(listOf(this)) { transform(value) } } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValObserver.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValObserver.kt deleted file mode 100644 index 80474585..00000000 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/ValObserver.kt +++ /dev/null @@ -1,9 +0,0 @@ -package world.phantasmal.observable.value - -import world.phantasmal.observable.ChangeEvent - -class ValChangeEvent(value: T, val oldValue: T) : ChangeEvent(value) { - operator fun component2() = oldValue -} - -typealias ValObserver = (event: ValChangeEvent) -> Unit diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractListVal.kt new file mode 100644 index 00000000..61b4579d --- /dev/null +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/AbstractListVal.kt @@ -0,0 +1,135 @@ +package world.phantasmal.observable.value.list + +import world.phantasmal.core.disposable.Disposable +import world.phantasmal.core.disposable.disposable +import world.phantasmal.observable.ChangeEvent +import world.phantasmal.observable.Observable +import world.phantasmal.observable.Observer +import world.phantasmal.observable.value.AbstractVal +import world.phantasmal.observable.value.MutableVal +import world.phantasmal.observable.value.Val +import world.phantasmal.observable.value.mutableVal + +abstract class AbstractListVal( + protected val elements: MutableList, + private val extractObservables: ObservablesExtractor? , +): AbstractVal>(), ListVal { + /** + * Internal observers which observe observables related to this list's elements so that their + * changes can be propagated via ElementChange events. + */ + private val elementObservers = mutableListOf() + + /** + * External list observers which are observing this list. + */ + protected val listObservers = mutableListOf>() + + override fun observe(callNow: Boolean, observer: Observer>): Disposable { + if (elementObservers.isEmpty() && extractObservables != null) { + replaceElementObservers(0, elementObservers.size, elements) + } + + observers.add(observer) + + if (callNow) { + observer(ChangeEvent(elements)) + } + + return disposable { + observers.remove(observer) + disposeElementObserversIfNecessary() + } + } + + override fun observeList(callNow: Boolean, observer: ListValObserver): Disposable { + if (elementObservers.isEmpty() && extractObservables != null) { + replaceElementObservers(0, elementObservers.size, elements) + } + + listObservers.add(observer) + + if (callNow) { + observer(ListValChangeEvent.Change(0, emptyList(), elements)) + } + + return disposable { + listObservers.remove(observer) + disposeElementObserversIfNecessary() + } + } + + /** + * Does the following in the given order: + * - Updates element observers + * - Emits ListValChangeEvent + * - Emits ValChangeEvent + */ + protected open fun finalizeUpdate(event: ListValChangeEvent) { + if ( + (listObservers.isNotEmpty() || observers.isNotEmpty()) && + extractObservables != null && + event is ListValChangeEvent.Change + ) { + replaceElementObservers(event.index, event.removed.size, event.inserted) + } + + listObservers.forEach { observer: ListValObserver -> + observer(event) + } + + emit() + } + + private fun replaceElementObservers(from: Int, amountRemoved: Int, insertedElements: List) { + for (i in 1..amountRemoved) { + elementObservers.removeAt(from).observers.forEach { it.dispose() } + } + + var index = from + + elementObservers.addAll( + from, + insertedElements.map { element -> + ElementObserver( + index++, + element, + extractObservables!!(element) + ) + } + ) + + val shift = insertedElements.size - amountRemoved + + while (index < elementObservers.size) { + elementObservers[index++].index += shift + } + } + + private fun disposeElementObserversIfNecessary() { + if (listObservers.isEmpty() && observers.isEmpty()) { + elementObservers.forEach { elementObserver: ElementObserver -> + elementObserver.observers.forEach { it.dispose() } + } + + elementObservers.clear() + } + } + + private inner class ElementObserver( + var index: Int, + element: E, + observables: Array>, + ) { + val observers: Array = Array(observables.size) { + observables[it].observe { + finalizeUpdate( + ListValChangeEvent.ElementChange( + index, + listOf(element) + ) + ) + } + } + } +} diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/DependentListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/DependentListVal.kt new file mode 100644 index 00000000..32970567 --- /dev/null +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/DependentListVal.kt @@ -0,0 +1,129 @@ +package world.phantasmal.observable.value.list + +import world.phantasmal.core.disposable.Disposable +import world.phantasmal.core.disposable.disposable +import world.phantasmal.observable.Observer +import world.phantasmal.observable.value.AbstractVal +import world.phantasmal.observable.value.Val + +/** + * Starts observing its dependencies when the first observer on this property is registered. + * Stops observing its dependencies when the last observer on this property is disposed. + * This way no extra disposables need to be managed when e.g. [map] is used. + */ +class DependentListVal( + private val dependencies: List>, + private val computeElements: () -> List, +) : AbstractListVal(mutableListOf(), extractObservables = null) { + private val _sizeVal = SizeVal() + + /** + * Set to true right before actual observers are added. + */ + private var hasObservers = false + + /** + * Is either empty or has a disposable per dependency. + */ + private val dependencyObservers = mutableListOf() + + override val value: List + get() { + if (!hasObservers) { + recompute() + } + + return elements + } + + override val sizeVal: Val = _sizeVal + + override fun observe(callNow: Boolean, observer: Observer>): Disposable { + initDependencyObservers() + + val superDisposable = super.observe(callNow, observer) + + return disposable { + superDisposable.dispose() + disposeDependencyObservers() + } + } + + override fun observeList(callNow: Boolean, observer: ListValObserver): Disposable { + initDependencyObservers() + + val superDisposable = super.observeList(callNow, observer) + + return disposable { + superDisposable.dispose() + disposeDependencyObservers() + } + } + + private fun recompute() { + elements.clear() + elements.addAll(computeElements()) + } + + private fun initDependencyObservers() { + if (dependencyObservers.isEmpty()) { + hasObservers = true + + dependencies.forEach { dependency -> + dependencyObservers.add( + dependency.observe { + val removed = ArrayList(elements) + recompute() + finalizeUpdate(ListValChangeEvent.Change(0, removed, elements)) + } + ) + } + + recompute() + } + } + + private fun disposeDependencyObservers() { + if (observers.isEmpty() && listObservers.isEmpty() && _sizeVal.publicObservers.isEmpty()) { + hasObservers = false + dependencyObservers.forEach { it.dispose() } + dependencyObservers.clear() + } + } + + override fun finalizeUpdate(event: ListValChangeEvent) { + if (event is ListValChangeEvent.Change && event.removed.size != event.inserted.size) { + _sizeVal.publicEmit() + } + + super.finalizeUpdate(event) + } + + private inner class SizeVal : AbstractVal() { + override val value: Int + get() { + if (!hasObservers) { + recompute() + } + + return elements.size + } + + val publicObservers = super.observers + + override fun observe(callNow: Boolean, observer: Observer): Disposable { + initDependencyObservers() + + val superDisposable = super.observe(callNow, observer) + + return disposable { + superDisposable.dispose() + disposeDependencyObservers() + } + } + + fun publicEmit() { + super.emit() + } + } +} diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/FoldedVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/FoldedVal.kt index 707707a3..feb2249c 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/FoldedVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/FoldedVal.kt @@ -3,8 +3,8 @@ package world.phantasmal.observable.value.list import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.disposable import world.phantasmal.core.unsafeToNonNull +import world.phantasmal.observable.Observer import world.phantasmal.observable.value.AbstractVal -import world.phantasmal.observable.value.ValObserver class FoldedVal( private val dependency: ListVal, @@ -23,16 +23,15 @@ class FoldedVal( } } - override fun observe(callNow: Boolean, observer: ValObserver): Disposable { + override fun observe(callNow: Boolean, observer: Observer): Disposable { val superDisposable = super.observe(callNow, observer) if (dependencyDisposable == null) { internalValue = computeValue() dependencyDisposable = dependency.observe { - val oldValue = internalValue internalValue = computeValue() - emit(oldValue.unsafeToNonNull()) + emit() } } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt index fdee657b..78208686 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/ListVal.kt @@ -13,4 +13,7 @@ interface ListVal : Val> { fun fold(initialValue: R, operation: (R, E) -> R): Val = FoldedVal(this, initialValue, operation) + + fun filtered(predicate: (E) -> Boolean): ListVal = + DependentListVal(listOf(this)) { value.filter(predicate) } } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt index 41515120..dc12f34d 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/SimpleListVal.kt @@ -1,54 +1,29 @@ package world.phantasmal.observable.value.list -import world.phantasmal.core.disposable.Disposable -import world.phantasmal.core.disposable.disposable import world.phantasmal.observable.Observable -import world.phantasmal.observable.Observer -import world.phantasmal.observable.value.* +import world.phantasmal.observable.value.MutableVal +import world.phantasmal.observable.value.Val +import world.phantasmal.observable.value.mutableVal typealias ObservablesExtractor = (element: E) -> Array> +/** + * @param elements The backing list for this ListVal + * @param extractObservables Extractor function called on each element in this list, changes to the + * returned observables will be propagated via ElementChange events + */ class SimpleListVal( - private val elements: MutableList, - /** - * Extractor function called on each element in this list. Changes to the returned observables - * will be propagated via ElementChange events. - */ - private val extractObservables: ObservablesExtractor? = null, -) : MutableListVal { + elements: MutableList, + extractObservables: ObservablesExtractor? = null, +) : AbstractListVal(elements, extractObservables), MutableListVal { + private val _sizeVal: MutableVal = mutableVal(elements.size) + override var value: List = elements set(value) { - val removed = ArrayList(elements) - elements.clear() - elements.addAll(value) - finalizeUpdate( - ListValChangeEvent.Change( - index = 0, - removed = removed, - inserted = value - ) - ) + replaceAll(value) } - private val mutableSizeVal: MutableVal = mutableVal(elements.size) - - override val sizeVal: Val = mutableSizeVal - - /** - * Internal observers which observe observables related to this list's elements so that their - * changes can be propagated via ElementChange events. - */ - private val elementObservers = mutableListOf() - - /** - * External list observers which are observing this list. - */ - private val listObservers = mutableListOf>() - - /** - * External regular observers which are observing this list. - */ - private val observers = mutableListOf>>() + override val sizeVal: Val = _sizeVal override fun set(index: Int, element: E): E { val removed = elements.set(index, element) @@ -93,121 +68,8 @@ class SimpleListVal( finalizeUpdate(ListValChangeEvent.Change(0, removed, emptyList())) } - override fun observe(observer: Observer>): Disposable = - observe(callNow = false, observer) - - override fun observe(callNow: Boolean, observer: ValObserver>): Disposable { - if (elementObservers.isEmpty() && extractObservables != null) { - replaceElementObservers(0, elementObservers.size, elements) - } - - observers.add(observer) - - if (callNow) { - observer(ValChangeEvent(elements, elements)) - } - - return disposable { - observers.remove(observer) - disposeElementObserversIfNecessary() - } - } - - override fun observeList(callNow: Boolean, observer: ListValObserver): Disposable { - if (elementObservers.isEmpty() && extractObservables != null) { - replaceElementObservers(0, elementObservers.size, elements) - } - - listObservers.add(observer) - - if (callNow) { - observer(ListValChangeEvent.Change(0, emptyList(), elements)) - } - - return disposable { - listObservers.remove(observer) - disposeElementObserversIfNecessary() - } - } - - /** - * Does the following in the given order: - * - Updates element observers - * - Emits size ValChangeEvent if necessary - * - Emits ListValChangeEvent - * - Emits ValChangeEvent - */ - private fun finalizeUpdate(event: ListValChangeEvent) { - if ( - (listObservers.isNotEmpty() || observers.isNotEmpty()) && - extractObservables != null && - event is ListValChangeEvent.Change - ) { - replaceElementObservers(event.index, event.removed.size, event.inserted) - } - - mutableSizeVal.value = elements.size - - listObservers.forEach { observer: ListValObserver -> - observer(event) - } - - val regularEvent = ValChangeEvent(elements, elements) - - observers.forEach { observer: ValObserver> -> - observer(regularEvent) - } - } - - private fun replaceElementObservers(from: Int, amountRemoved: Int, insertedElements: List) { - for (i in 1..amountRemoved) { - elementObservers.removeAt(from).observers.forEach { it.dispose() } - } - - var index = from - - elementObservers.addAll( - from, - insertedElements.map { element -> - ElementObserver( - index++, - element, - extractObservables!!(element) - ) - } - ) - - val shift = insertedElements.size - amountRemoved - - while (index < elementObservers.size) { - elementObservers[index++].index += shift - } - } - - private fun disposeElementObserversIfNecessary() { - if (listObservers.isEmpty() && observers.isEmpty()) { - elementObservers.forEach { elementObserver: ElementObserver -> - elementObserver.observers.forEach { it.dispose() } - } - - elementObservers.clear() - } - } - - private inner class ElementObserver( - var index: Int, - element: E, - observables: Array>, - ) { - val observers: Array = Array(observables.size) { - observables[it].observe { - finalizeUpdate( - ListValChangeEvent.ElementChange( - index, - listOf(element) - ) - ) - } - } + override fun finalizeUpdate(event: ListValChangeEvent) { + _sizeVal.value = elements.size + super.finalizeUpdate(event) } } diff --git a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/StaticListVal.kt b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/StaticListVal.kt index d5b5cd7d..4c704eb9 100644 --- a/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/StaticListVal.kt +++ b/observable/src/commonMain/kotlin/world/phantasmal/observable/value/list/StaticListVal.kt @@ -2,10 +2,9 @@ package world.phantasmal.observable.value.list import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.stubDisposable +import world.phantasmal.observable.ChangeEvent import world.phantasmal.observable.Observer import world.phantasmal.observable.value.Val -import world.phantasmal.observable.value.ValChangeEvent -import world.phantasmal.observable.value.ValObserver import world.phantasmal.observable.value.value class StaticListVal(elements: List) : ListVal { @@ -13,9 +12,9 @@ class StaticListVal(elements: List) : ListVal { override val value: List = elements - override fun observe(callNow: Boolean, observer: ValObserver>): Disposable { + override fun observe(callNow: Boolean, observer: Observer>): Disposable { if (callNow) { - observer(ValChangeEvent(value, value)) + observer(ChangeEvent(value)) } return stubDisposable() diff --git a/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/DependentListValTests.kt b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/DependentListValTests.kt new file mode 100644 index 00000000..6a7b4045 --- /dev/null +++ b/observable/src/commonTest/kotlin/world/phantasmal/observable/value/list/DependentListValTests.kt @@ -0,0 +1,9 @@ +package world.phantasmal.observable.value.list + +class DependentListValTests : ListValTests() { + override fun create(): ListValAndAdd { + val l = SimpleListVal(mutableListOf()) + val list = DependentListVal(listOf(l)) { l.value.map { 2 * it } } + return ListValAndAdd(list) { l.add(4) } + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt index 430c9960..50d1f68b 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/QuestEditor.kt @@ -63,7 +63,7 @@ class QuestEditor( // Main Widget return QuestEditorWidget( scope, - { s -> QuestEditorToolbar(s, toolbarController) }, + { s -> QuestEditorToolbarWidget(s, toolbarController) }, { s -> QuestInfoWidget(s, questInfoController) }, { s -> NpcCountsWidget(s, npcCountsController) }, { s -> QuestEditorRendererWidget(s, canvas, renderer) } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt index d3ec4581..18960a70 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/controllers/QuestEditorToolbarController.kt @@ -11,7 +11,9 @@ import world.phantasmal.lib.fileFormats.quest.parseBinDatToQuest import world.phantasmal.lib.fileFormats.quest.parseQstToQuest import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.mutableVal +import world.phantasmal.observable.value.value import world.phantasmal.web.questEditor.loading.QuestLoader +import world.phantasmal.web.questEditor.models.AreaModel import world.phantasmal.web.questEditor.stores.AreaStore import world.phantasmal.web.questEditor.stores.QuestEditorStore import world.phantasmal.web.questEditor.stores.convertQuestToModel @@ -20,6 +22,8 @@ import world.phantasmal.webui.readFile private val logger = KotlinLogging.logger {} +class AreaAndLabel(val area: AreaModel, val label: String) + class QuestEditorToolbarController( private val questLoader: QuestLoader, private val areaStore: AreaStore, @@ -31,6 +35,28 @@ class QuestEditorToolbarController( val resultDialogVisible: Val = _resultDialogVisible val result: Val?> = _result + // Ensure the areas list is updated when entities are added or removed (the count in the + // label should update). + val areas: Val> = questEditorStore.currentQuest.flatMap { quest -> + quest?.let { + quest.entitiesPerArea.map { entitiesPerArea -> + areaStore.getAreasForEpisode(quest.episode).map { area -> + val entityCount = entitiesPerArea[area.id] + AreaAndLabel(area, area.name + (entityCount?.let { " ($it)" } ?: "")) + } + } + } ?: value(emptyList()) + } + val currentArea: Val = areas.map(questEditorStore.currentArea) { areas, area -> + areas.find { it.area == area } + } + val areaSelectDisabled: Val + + init { + val noQuestLoaded = questEditorStore.currentQuest.map { it == null } + areaSelectDisabled = noQuestLoaded + } + suspend fun createNewQuest(episode: Episode) { questEditorStore.setCurrentQuest( convertQuestToModel(questLoader.loadDefaultQuest(episode), areaStore::getVariant) @@ -81,6 +107,10 @@ class QuestEditorToolbarController( } } + fun setCurrentArea(areaAndLabel: AreaAndLabel) { + questEditorStore.setCurrentArea(areaAndLabel.area) + } + private suspend fun setCurrentQuest(quest: Quest) { questEditorStore.setCurrentQuest(convertQuestToModel(quest, areaStore::getVariant)) } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestEntityModel.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestEntityModel.kt index 67d8225f..76a00231 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestEntityModel.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestEntityModel.kt @@ -18,6 +18,7 @@ abstract class QuestEntityModel>( ) { private val _sectionId = mutableVal(entity.sectionId) private val _section = mutableVal(null) + private val _sectionInitialized = mutableVal(false) private val _position = mutableVal(vec3ToBabylon(entity.position)) private val _worldPosition = mutableVal(_position.value) private val _rotation = mutableVal(vec3ToBabylon(entity.rotation)) @@ -30,6 +31,7 @@ abstract class QuestEntityModel>( val sectionId: Val = _sectionId val section: Val = _section + val sectionInitialized: Val = _sectionInitialized /** * Section-relative position @@ -57,6 +59,12 @@ abstract class QuestEntityModel>( setPosition(position.value) setRotation(rotation.value) + + setSectionInitialized() + } + + fun setSectionInitialized() { + _sectionInitialized.value = true } fun setPosition(pos: Vector3) { diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestObjectModel.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestObjectModel.kt index 2c5d8caa..800d3d67 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestObjectModel.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/models/QuestObjectModel.kt @@ -2,5 +2,17 @@ package world.phantasmal.web.questEditor.models import world.phantasmal.lib.fileFormats.quest.ObjectType import world.phantasmal.lib.fileFormats.quest.QuestObject +import world.phantasmal.observable.value.Val +import world.phantasmal.observable.value.mutableVal -class QuestObjectModel(obj: QuestObject) : QuestEntityModel(obj) +class QuestObjectModel(obj: QuestObject) : QuestEntityModel(obj) { + private val _model = mutableVal(obj.model) + + val model: Val = _model + + fun setModel(model: Int) { + _model.value = model + + // TODO: Propagate to props. + } +} diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt index 53bf492c..f1a2dbfc 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/EntityMeshManager.kt @@ -9,6 +9,7 @@ import world.phantasmal.web.externals.babylon.TransformNode import world.phantasmal.web.questEditor.loading.EntityAssetLoader import world.phantasmal.web.questEditor.models.QuestEntityModel import world.phantasmal.web.questEditor.models.QuestNpcModel +import world.phantasmal.web.questEditor.models.QuestObjectModel import world.phantasmal.web.questEditor.models.WaveModel import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata import world.phantasmal.web.questEditor.stores.QuestEditorStore @@ -117,8 +118,10 @@ class EntityMeshManager( } private suspend fun load(entity: QuestEntityModel<*, *>) { - // TODO - val mesh = entityAssetLoader.loadMesh(entity.type, model = null) + val mesh = entityAssetLoader.loadMesh( + type = entity.type, + model = (entity as? QuestObjectModel)?.model?.value + ) // Only add an instance of this mesh if the entity is still in the queue at this point. if (queue.remove(entity)) { @@ -132,48 +135,53 @@ class EntityMeshManager( loadedEntities[entity] = LoadedEntity(entity, instance, questEditorStore.selectedWave) } } -} -private class LoadedEntity( - entity: QuestEntityModel<*, *>, - val mesh: AbstractMesh, - selectedWave: Val, -) : DisposableContainer() { - init { - mesh.metadata = EntityMetadata(entity) + private inner class LoadedEntity( + entity: QuestEntityModel<*, *>, + val mesh: AbstractMesh, + selectedWave: Val, + ) : DisposableContainer() { + init { + mesh.metadata = EntityMetadata(entity) - observe(entity.worldPosition) { pos -> - mesh.position = pos - } + observe(entity.worldPosition) { pos -> + mesh.position = pos + } - observe(entity.worldRotation) { rot -> - mesh.rotation = rot - } + observe(entity.worldRotation) { rot -> + mesh.rotation = rot + } - addDisposables( - // TODO: Model. -// entity.model.observe { -// remove(listOf(entity)) -// add(listOf(entity)) -// }, - ) + val isVisible: Val - if (entity is QuestNpcModel) { - addDisposable( - selectedWave - .map(entity.wave) { sWave, entityWave -> - sWave == null || sWave == entityWave + if (entity is QuestNpcModel) { + isVisible = + entity.sectionInitialized.map( + selectedWave, + entity.wave + ) { sectionInitialized, sWave, entityWave -> + sectionInitialized && (sWave == null || sWave == entityWave) } - .observe(callNow = true) { (visible) -> - mesh.setEnabled(visible) - }, - ) + } else { + isVisible = entity.section.map { section -> section != null } + + if (entity is QuestObjectModel) { + addDisposable(entity.model.observe(callNow = false) { + remove(listOf(entity)) + add(listOf(entity)) + }) + } + } + + observe(isVisible) { visible -> + mesh.setEnabled(visible) + } + } + + override fun internalDispose() { + mesh.parent = null + mesh.dispose() + super.internalDispose() } } - - override fun internalDispose() { - mesh.parent = null - mesh.dispose() - super.internalDispose() - } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestEditorMeshManager.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestEditorMeshManager.kt index e9e246c2..cb521f56 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestEditorMeshManager.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestEditorMeshManager.kt @@ -28,12 +28,12 @@ class QuestEditorMeshManager( private fun getAreaVariantDetails(quest: QuestModel?, area: AreaModel?): AreaVariantDetails { quest?.let { val areaVariant = area?.let { - quest.areaVariants.value.find { it.area.id == area.id } + quest.areaVariants.value.find { it.area.id == area.id } ?: area.areaVariants.first() } areaVariant?.let { - val npcs = quest.npcs // TODO: Filter NPCs. - val objects = quest.objects // TODO: Filter objects. + val npcs = quest.npcs.filtered { it.areaId == area.id } + val objects = quest.objects.filtered { it.areaId == area.id } return AreaVariantDetails(quest.episode, areaVariant, npcs, objects) } } diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt index 070d79ff..b24b27d6 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/rendering/QuestMeshManager.kt @@ -21,7 +21,7 @@ import world.phantasmal.web.questEditor.stores.QuestEditorStore abstract class QuestMeshManager protected constructor( private val scope: CoroutineScope, questEditorStore: QuestEditorStore, - private val renderer: QuestRenderer, + renderer: QuestRenderer, areaAssetLoader: AreaAssetLoader, entityAssetLoader: EntityAssetLoader, ) : TrackedDisposable() { @@ -46,12 +46,14 @@ abstract class QuestMeshManager protected constructor( ) { loadJob?.cancel() loadJob = scope.launch { - areaMeshManager.load(episode, areaVariant) - + // Reset models. areaDisposer.disposeAll() npcMeshManager.removeAll() objectMeshManager.removeAll() + // Load area model. + areaMeshManager.load(episode, areaVariant) + // Load entity meshes. areaDisposer.addAll( npcs.observeList(callNow = true, ::npcsChanged), diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt index ab65322e..b6bc0fa2 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/stores/QuestEditorStore.kt @@ -24,11 +24,12 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore) val questEditingDisabled: Val = currentQuest.map { it == null } suspend fun setCurrentQuest(quest: QuestModel?) { - _currentArea.value = null - _currentQuest.value = quest - - quest?.let { + if (quest == null) { + _currentArea.value = null + _currentQuest.value = null + } else { _currentArea.value = areaStore.getArea(quest.episode, 0) + _currentQuest.value = quest // Load section data. quest.areaVariants.value.forEach { variant -> @@ -37,6 +38,10 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore) setSectionOnQuestEntities(quest.npcs.value, variant, sections) setSectionOnQuestEntities(quest.objects.value, variant, sections) } + + // Ensure all entities have their section initialized. + quest.npcs.value.forEach { it.setSectionInitialized() } + quest.objects.value.forEach { it.setSectionInitialized() } } } @@ -51,6 +56,7 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore) if (section == null) { logger.warn { "Section ${entity.sectionId.value} not found." } + entity.setSectionInitialized() } else { entity.setSection(section) } @@ -58,6 +64,13 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore) } } + fun setCurrentArea(area: AreaModel?) { + // TODO: Set wave. + + _selectedEntity.value = null + _currentArea.value = area + } + fun setSelectedEntity(entity: QuestEntityModel<*, *>?) { entity?.let { currentQuest.value?.let { quest -> diff --git a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorToolbar.kt b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorToolbarWidget.kt similarity index 75% rename from web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorToolbar.kt rename to web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorToolbarWidget.kt index db510ae3..ac554b3f 100644 --- a/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorToolbar.kt +++ b/web/src/main/kotlin/world/phantasmal/web/questEditor/widgets/QuestEditorToolbarWidget.kt @@ -7,12 +7,9 @@ import world.phantasmal.lib.fileFormats.quest.Episode import world.phantasmal.web.questEditor.controllers.QuestEditorToolbarController import world.phantasmal.webui.dom.Icon import world.phantasmal.webui.dom.div -import world.phantasmal.webui.widgets.Button -import world.phantasmal.webui.widgets.FileButton -import world.phantasmal.webui.widgets.Toolbar -import world.phantasmal.webui.widgets.Widget +import world.phantasmal.webui.widgets.* -class QuestEditorToolbar( +class QuestEditorToolbarWidget( scope: CoroutineScope, private val ctrl: QuestEditorToolbarController, ) : Widget(scope) { @@ -36,6 +33,14 @@ class QuestEditorToolbar( accept = ".bin, .dat, .qst", multiple = true, filesSelected = { files -> scope.launch { ctrl.openFiles(files) } } + ), + Select( + scope, + disabled = ctrl.areaSelectDisabled, + itemsVal = ctrl.areas, + itemToString = { it.label }, + selectedVal = ctrl.currentArea, + onSelect = ctrl::setCurrentArea ) ) )) diff --git a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Select.kt b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Select.kt index c69fa1ac..4ad8bea0 100644 --- a/webui/src/main/kotlin/world/phantasmal/webui/widgets/Select.kt +++ b/webui/src/main/kotlin/world/phantasmal/webui/widgets/Select.kt @@ -37,8 +37,7 @@ class Select( private val items: Val> = itemsVal ?: value(items ?: emptyList()) private val selected: Val = selectedVal ?: value(selected) - // Default to a single space so the inner text part won't be hidden. - private val buttonText = mutableVal(this.selected.value?.let(itemToString) ?: " ") + private val buttonText = mutableVal(" ") private val menuHidden = mutableVal(true) private lateinit var menu: Menu @@ -48,6 +47,9 @@ class Select( div { className = "pw-select" + // Default to a single space so the inner text part won't be hidden. + observe(selected) { buttonText.value = it?.let(itemToString) ?: " " } + addWidget(Button( scope, disabled = disabled,