mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Entities are now shown per area and area selection is now possible. Fixed some bugs.
This commit is contained in:
parent
db1149ddc0
commit
132cdccd0a
@ -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
|
||||
|
@ -20,9 +20,9 @@ class QuestObject(override var areaId: Int, val data: Buffer) : QuestEntity<Obje
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -2,19 +2,20 @@ package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.core.disposable.disposable
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
import world.phantasmal.observable.Observer
|
||||
|
||||
abstract class AbstractVal<T> : Val<T> {
|
||||
protected val observers: MutableList<ValObserver<T>> = mutableListOf()
|
||||
protected val observers: MutableList<Observer<T>> = mutableListOf()
|
||||
|
||||
final override fun observe(observer: Observer<T>): Disposable =
|
||||
observe(callNow = false, observer)
|
||||
|
||||
override fun observe(callNow: Boolean, observer: ValObserver<T>): Disposable {
|
||||
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
|
||||
observers.add(observer)
|
||||
|
||||
if (callNow) {
|
||||
observer(ValChangeEvent(value, value))
|
||||
observer(ChangeEvent(value))
|
||||
}
|
||||
|
||||
return disposable {
|
||||
@ -22,8 +23,8 @@ abstract class AbstractVal<T> : Val<T> {
|
||||
}
|
||||
}
|
||||
|
||||
protected fun emit(oldValue: T) {
|
||||
val event = ValChangeEvent(value, oldValue)
|
||||
protected fun emit() {
|
||||
val event = ChangeEvent(value)
|
||||
observers.forEach { it(event) }
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ class DelegatingVal<T>(
|
||||
|
||||
if (value != oldValue) {
|
||||
setter(value)
|
||||
emit(oldValue)
|
||||
emit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<T>(
|
||||
*/
|
||||
private val dependencyObservers = mutableListOf<Disposable>()
|
||||
|
||||
/**
|
||||
* 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<T>): Disposable {
|
||||
if (hasNoObservers()) {
|
||||
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
|
||||
if (dependencyObservers.isEmpty()) {
|
||||
hasObservers = true
|
||||
|
||||
dependencies.forEach { dependency ->
|
||||
dependencyObservers.add(
|
||||
dependency.observe {
|
||||
@ -37,7 +45,7 @@ abstract class DependentVal<T>(
|
||||
_value = computeValue()
|
||||
|
||||
if (_value != oldValue) {
|
||||
emit(oldValue.unsafeToNonNull())
|
||||
emit()
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -52,17 +60,12 @@ abstract class DependentVal<T>(
|
||||
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
|
||||
}
|
||||
|
@ -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<T>(
|
||||
dependencies: Iterable<Val<*>>,
|
||||
@ -13,20 +14,20 @@ class FlatMappedVal<T>(
|
||||
|
||||
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<T>): Disposable {
|
||||
override fun observe(callNow: Boolean, observer: Observer<T>): 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<T>(
|
||||
|
||||
computedValObserver?.dispose()
|
||||
|
||||
if (hasObservers()) {
|
||||
if (hasObservers) {
|
||||
computedValObserver = computedVal.observe { (value) ->
|
||||
val oldValue = _value.unsafeToNonNull<T>()
|
||||
_value = value
|
||||
emit(oldValue)
|
||||
emit()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,8 @@ class SimpleVal<T>(value: T) : AbstractVal<T>(), MutableVal<T> {
|
||||
override var value: T = value
|
||||
set(value) {
|
||||
if (value != field) {
|
||||
val oldValue = field
|
||||
field = value
|
||||
emit(oldValue)
|
||||
emit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<T>(override val value: T) : Val<T> {
|
||||
override fun observe(callNow: Boolean, observer: ValObserver<T>): Disposable {
|
||||
override fun observe(callNow: Boolean, observer: Observer<T>): Disposable {
|
||||
if (callNow) {
|
||||
observer(ValChangeEvent(value, value))
|
||||
observer(ChangeEvent(value))
|
||||
}
|
||||
|
||||
return stubDisposable()
|
||||
|
@ -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<out T> : Observable<T> {
|
||||
/**
|
||||
* @param callNow Call [observer] immediately with the current [mutableVal].
|
||||
*/
|
||||
fun observe(callNow: Boolean = false, observer: ValObserver<T>): Disposable
|
||||
fun observe(callNow: Boolean = false, observer: Observer<T>): Disposable
|
||||
|
||||
fun <R> map(transform: (T) -> R): Val<R> =
|
||||
MappedVal(listOf(this)) { transform(value) }
|
||||
@ -23,6 +24,9 @@ interface Val<out T> : Observable<T> {
|
||||
fun <T2, R> map(v2: Val<T2>, transform: (T, T2) -> R): Val<R> =
|
||||
MappedVal(listOf(this, v2)) { transform(value, v2.value) }
|
||||
|
||||
fun <T2, T3, R> map(v2: Val<T2>, v3: Val<T3>, transform: (T, T2, T3) -> R): Val<R> =
|
||||
MappedVal(listOf(this, v2, v3)) { transform(value, v2.value, v3.value) }
|
||||
|
||||
fun <R> flatMap(transform: (T) -> Val<R>): Val<R> =
|
||||
FlatMappedVal(listOf(this)) { transform(value) }
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
package world.phantasmal.observable.value
|
||||
|
||||
import world.phantasmal.observable.ChangeEvent
|
||||
|
||||
class ValChangeEvent<out T>(value: T, val oldValue: T) : ChangeEvent<T>(value) {
|
||||
operator fun component2() = oldValue
|
||||
}
|
||||
|
||||
typealias ValObserver<T> = (event: ValChangeEvent<T>) -> Unit
|
@ -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<E>(
|
||||
protected val elements: MutableList<E>,
|
||||
private val extractObservables: ObservablesExtractor<E>? ,
|
||||
): AbstractVal<List<E>>(), ListVal<E> {
|
||||
/**
|
||||
* 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<ElementObserver>()
|
||||
|
||||
/**
|
||||
* External list observers which are observing this list.
|
||||
*/
|
||||
protected val listObservers = mutableListOf<ListValObserver<E>>()
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<List<E>>): 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<E>): 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<E>) {
|
||||
if (
|
||||
(listObservers.isNotEmpty() || observers.isNotEmpty()) &&
|
||||
extractObservables != null &&
|
||||
event is ListValChangeEvent.Change
|
||||
) {
|
||||
replaceElementObservers(event.index, event.removed.size, event.inserted)
|
||||
}
|
||||
|
||||
listObservers.forEach { observer: ListValObserver<E> ->
|
||||
observer(event)
|
||||
}
|
||||
|
||||
emit()
|
||||
}
|
||||
|
||||
private fun replaceElementObservers(from: Int, amountRemoved: Int, insertedElements: List<E>) {
|
||||
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<Observable<*>>,
|
||||
) {
|
||||
val observers: Array<Disposable> = Array(observables.size) {
|
||||
observables[it].observe {
|
||||
finalizeUpdate(
|
||||
ListValChangeEvent.ElementChange(
|
||||
index,
|
||||
listOf(element)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<E>(
|
||||
private val dependencies: List<Val<*>>,
|
||||
private val computeElements: () -> List<E>,
|
||||
) : AbstractListVal<E>(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<Disposable>()
|
||||
|
||||
override val value: List<E>
|
||||
get() {
|
||||
if (!hasObservers) {
|
||||
recompute()
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
override val sizeVal: Val<Int> = _sizeVal
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
|
||||
initDependencyObservers()
|
||||
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
disposeDependencyObservers()
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeList(callNow: Boolean, observer: ListValObserver<E>): 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<E>) {
|
||||
if (event is ListValChangeEvent.Change && event.removed.size != event.inserted.size) {
|
||||
_sizeVal.publicEmit()
|
||||
}
|
||||
|
||||
super.finalizeUpdate(event)
|
||||
}
|
||||
|
||||
private inner class SizeVal : AbstractVal<Int>() {
|
||||
override val value: Int
|
||||
get() {
|
||||
if (!hasObservers) {
|
||||
recompute()
|
||||
}
|
||||
|
||||
return elements.size
|
||||
}
|
||||
|
||||
val publicObservers = super.observers
|
||||
|
||||
override fun observe(callNow: Boolean, observer: Observer<Int>): Disposable {
|
||||
initDependencyObservers()
|
||||
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
|
||||
return disposable {
|
||||
superDisposable.dispose()
|
||||
disposeDependencyObservers()
|
||||
}
|
||||
}
|
||||
|
||||
fun publicEmit() {
|
||||
super.emit()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<T, R>(
|
||||
private val dependency: ListVal<T>,
|
||||
@ -23,16 +23,15 @@ class FoldedVal<T, R>(
|
||||
}
|
||||
}
|
||||
|
||||
override fun observe(callNow: Boolean, observer: ValObserver<R>): Disposable {
|
||||
override fun observe(callNow: Boolean, observer: Observer<R>): Disposable {
|
||||
val superDisposable = super.observe(callNow, observer)
|
||||
|
||||
if (dependencyDisposable == null) {
|
||||
internalValue = computeValue()
|
||||
|
||||
dependencyDisposable = dependency.observe {
|
||||
val oldValue = internalValue
|
||||
internalValue = computeValue()
|
||||
emit(oldValue.unsafeToNonNull())
|
||||
emit()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,4 +13,7 @@ interface ListVal<E> : Val<List<E>> {
|
||||
|
||||
fun <R> fold(initialValue: R, operation: (R, E) -> R): Val<R> =
|
||||
FoldedVal(this, initialValue, operation)
|
||||
|
||||
fun filtered(predicate: (E) -> Boolean): ListVal<E> =
|
||||
DependentListVal(listOf(this)) { value.filter(predicate) }
|
||||
}
|
||||
|
@ -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<E> = (element: E) -> Array<Observable<*>>
|
||||
|
||||
class SimpleListVal<E>(
|
||||
private val elements: MutableList<E>,
|
||||
/**
|
||||
* Extractor function called on each element in this list. Changes to the returned observables
|
||||
* will be propagated via ElementChange events.
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
private val extractObservables: ObservablesExtractor<E>? = null,
|
||||
) : MutableListVal<E> {
|
||||
class SimpleListVal<E>(
|
||||
elements: MutableList<E>,
|
||||
extractObservables: ObservablesExtractor<E>? = null,
|
||||
) : AbstractListVal<E>(elements, extractObservables), MutableListVal<E> {
|
||||
private val _sizeVal: MutableVal<Int> = mutableVal(elements.size)
|
||||
|
||||
override var value: List<E> = 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<Int> = mutableVal(elements.size)
|
||||
|
||||
override val sizeVal: Val<Int> = 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<ElementObserver>()
|
||||
|
||||
/**
|
||||
* External list observers which are observing this list.
|
||||
*/
|
||||
private val listObservers = mutableListOf<ListValObserver<E>>()
|
||||
|
||||
/**
|
||||
* External regular observers which are observing this list.
|
||||
*/
|
||||
private val observers = mutableListOf<ValObserver<List<E>>>()
|
||||
override val sizeVal: Val<Int> = _sizeVal
|
||||
|
||||
override fun set(index: Int, element: E): E {
|
||||
val removed = elements.set(index, element)
|
||||
@ -93,121 +68,8 @@ class SimpleListVal<E>(
|
||||
finalizeUpdate(ListValChangeEvent.Change(0, removed, emptyList()))
|
||||
}
|
||||
|
||||
override fun observe(observer: Observer<List<E>>): Disposable =
|
||||
observe(callNow = false, observer)
|
||||
|
||||
override fun observe(callNow: Boolean, observer: ValObserver<List<E>>): 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<E>): 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<E>) {
|
||||
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<E> ->
|
||||
observer(event)
|
||||
}
|
||||
|
||||
val regularEvent = ValChangeEvent(elements, elements)
|
||||
|
||||
observers.forEach { observer: ValObserver<List<E>> ->
|
||||
observer(regularEvent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceElementObservers(from: Int, amountRemoved: Int, insertedElements: List<E>) {
|
||||
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<Observable<*>>,
|
||||
) {
|
||||
val observers: Array<Disposable> = Array(observables.size) {
|
||||
observables[it].observe {
|
||||
finalizeUpdate(
|
||||
ListValChangeEvent.ElementChange(
|
||||
index,
|
||||
listOf(element)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
override fun finalizeUpdate(event: ListValChangeEvent<E>) {
|
||||
_sizeVal.value = elements.size
|
||||
super.finalizeUpdate(event)
|
||||
}
|
||||
}
|
||||
|
@ -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<E>(elements: List<E>) : ListVal<E> {
|
||||
@ -13,9 +12,9 @@ class StaticListVal<E>(elements: List<E>) : ListVal<E> {
|
||||
|
||||
override val value: List<E> = elements
|
||||
|
||||
override fun observe(callNow: Boolean, observer: ValObserver<List<E>>): Disposable {
|
||||
override fun observe(callNow: Boolean, observer: Observer<List<E>>): Disposable {
|
||||
if (callNow) {
|
||||
observer(ValChangeEvent(value, value))
|
||||
observer(ChangeEvent(value))
|
||||
}
|
||||
|
||||
return stubDisposable()
|
||||
|
@ -0,0 +1,9 @@
|
||||
package world.phantasmal.observable.value.list
|
||||
|
||||
class DependentListValTests : ListValTests() {
|
||||
override fun create(): ListValAndAdd {
|
||||
val l = SimpleListVal<Int>(mutableListOf())
|
||||
val list = DependentListVal(listOf(l)) { l.value.map { 2 * it } }
|
||||
return ListValAndAdd(list) { l.add(4) }
|
||||
}
|
||||
}
|
@ -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) }
|
||||
|
@ -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<Boolean> = _resultDialogVisible
|
||||
val result: Val<PwResult<*>?> = _result
|
||||
|
||||
// Ensure the areas list is updated when entities are added or removed (the count in the
|
||||
// label should update).
|
||||
val areas: Val<List<AreaAndLabel>> = 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<AreaAndLabel?> = areas.map(questEditorStore.currentArea) { areas, area ->
|
||||
areas.find { it.area == area }
|
||||
}
|
||||
val areaSelectDisabled: Val<Boolean>
|
||||
|
||||
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))
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
) {
|
||||
private val _sectionId = mutableVal(entity.sectionId)
|
||||
private val _section = mutableVal<SectionModel?>(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<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
val sectionId: Val<Int> = _sectionId
|
||||
|
||||
val section: Val<SectionModel?> = _section
|
||||
val sectionInitialized: Val<Boolean> = _sectionInitialized
|
||||
|
||||
/**
|
||||
* Section-relative position
|
||||
@ -57,6 +59,12 @@ abstract class QuestEntityModel<Type : EntityType, Entity : QuestEntity<Type>>(
|
||||
|
||||
setPosition(position.value)
|
||||
setRotation(rotation.value)
|
||||
|
||||
setSectionInitialized()
|
||||
}
|
||||
|
||||
fun setSectionInitialized() {
|
||||
_sectionInitialized.value = true
|
||||
}
|
||||
|
||||
fun setPosition(pos: Vector3) {
|
||||
|
@ -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<ObjectType, QuestObject>(obj)
|
||||
class QuestObjectModel(obj: QuestObject) : QuestEntityModel<ObjectType, QuestObject>(obj) {
|
||||
private val _model = mutableVal(obj.model)
|
||||
|
||||
val model: Val<Int?> = _model
|
||||
|
||||
fun setModel(model: Int) {
|
||||
_model.value = model
|
||||
|
||||
// TODO: Propagate to props.
|
||||
}
|
||||
}
|
||||
|
@ -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,13 +135,12 @@ class EntityMeshManager(
|
||||
loadedEntities[entity] = LoadedEntity(entity, instance, questEditorStore.selectedWave)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LoadedEntity(
|
||||
private inner class LoadedEntity(
|
||||
entity: QuestEntityModel<*, *>,
|
||||
val mesh: AbstractMesh,
|
||||
selectedWave: Val<WaveModel?>,
|
||||
) : DisposableContainer() {
|
||||
) : DisposableContainer() {
|
||||
init {
|
||||
mesh.metadata = EntityMetadata(entity)
|
||||
|
||||
@ -150,24 +152,29 @@ private class LoadedEntity(
|
||||
mesh.rotation = rot
|
||||
}
|
||||
|
||||
addDisposables(
|
||||
// TODO: Model.
|
||||
// entity.model.observe {
|
||||
// remove(listOf(entity))
|
||||
// add(listOf(entity))
|
||||
// },
|
||||
)
|
||||
val isVisible: Val<Boolean>
|
||||
|
||||
if (entity is QuestNpcModel) {
|
||||
addDisposable(
|
||||
selectedWave
|
||||
.map(entity.wave) { sWave, entityWave ->
|
||||
sWave == null || sWave == entityWave
|
||||
isVisible =
|
||||
entity.sectionInitialized.map(
|
||||
selectedWave,
|
||||
entity.wave
|
||||
) { sectionInitialized, sWave, entityWave ->
|
||||
sectionInitialized && (sWave == null || sWave == entityWave)
|
||||
}
|
||||
.observe(callNow = true) { (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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,4 +183,5 @@ private class LoadedEntity(
|
||||
mesh.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -24,11 +24,12 @@ class QuestEditorStore(scope: CoroutineScope, private val areaStore: AreaStore)
|
||||
val questEditingDisabled: Val<Boolean> = currentQuest.map { it == null }
|
||||
|
||||
suspend fun setCurrentQuest(quest: QuestModel?) {
|
||||
if (quest == null) {
|
||||
_currentArea.value = null
|
||||
_currentQuest.value = quest
|
||||
|
||||
quest?.let {
|
||||
_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 ->
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
))
|
@ -37,8 +37,7 @@ class Select<T : Any>(
|
||||
private val items: Val<List<T>> = itemsVal ?: value(items ?: emptyList())
|
||||
private val selected: Val<T?> = 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<T>
|
||||
@ -48,6 +47,9 @@ class Select<T : Any>(
|
||||
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,
|
||||
|
Loading…
Reference in New Issue
Block a user