Skip to content

Commit fd64509

Browse files
Version 1.4.1
1 parent 451d62f commit fd64509

43 files changed

Lines changed: 625 additions & 306 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build-logic/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ dependencies {
3838
implementation ("com.google.guava:guava:30.1.1-jre")
3939

4040
implementation("gg.essential:essential-gradle-toolkit:0.7.1")
41+
// TODO remove once EGT is updated
42+
implementation("com.github.replaymod:preprocessor:221276c7d!!")
4143
// TODO remove once preprocessor+EGT are updated
4244
implementation("com.github.replaymod:remap:651e2e0a2!!") {
4345
exclude("org.jetbrains.kotlin", "kotlin-compiler-embeddable")

build-logic/src/main/kotlin/essential/preprocessor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ fun Project.configurePreprocessTree(versions: File) {
2020
configure<RootPreprocessExtension> {
2121
strictExtraMappings.set(true)
2222

23-
val fabric260200 = createNode("26.2-fabric", 260200, "mojang")
24-
val fabric260100 = createNode("26.1-fabric", 260100, "mojang")
23+
val fabric260200 = createNode("26.2-fabric", 260200, null)
24+
val fabric260100 = createNode("26.1-fabric", 260100, null)
2525
val fabric12111 = createNode("1.21.11-fabric", 12111, "yarn")
2626
val fabric12109 = createNode("1.21.9-fabric", 12109, "yarn")
2727
val neoForge12107 = createNode("1.21.7-neoforge", 12107, "srg")

build-logic/src/main/kotlin/gg/essential/gradle/util/KotlinVersion.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ data class KotlinVersion(
2121
val serialization: String,
2222
) {
2323
companion object {
24+
// Note: When updating stdlib version, also update the check at the start of IntegrationTestBootstrap.initialize
2425
val latest = KotlinVersion(null, "2.3.0", "1.10.2", "1.9.0")
2526

2627
val fabricLanguageKotlin = latest.copy(mod = "1.13.8")

changelog/release-1.4.1.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Title: Appear offline
2+
Summary: Hide your online status
3+
4+
## Appear offline
5+
Hide your online status from your friends. When enabled, your activity status is hidden and you'll appear offline. Perfect for when you just want to be left alone.
6+
- You can enable this via the "Appear offline" toggle in the Friends tab of the Essential Settings menu
7+
8+
## Other Improvements
9+
- Added a "Friends" tab to the Essential settings. Here you can manage social settings like activity sharing, friend requests, and Essential chat
10+
11+
## Bug Fixes
12+
- Fixed editing screenshots which caused all entities, particles, and/or terrain to become invisible on certain Minecraft versions

elementa/statev2/src/main/kotlin/gg/essential/gui/elementa/state/v2/impl/basic/impl.kt

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,15 @@ import kotlin.collections.asSequence
3636
* Unlike [gg.essential.gui.elementa.state.v2.impl.minimal.MarkThenPullImpl], this means that sub-graphs which are
3737
* potentially affected but whose dependencies have not actually changed, will not be visited (more than once per
3838
* them actually changing; as opposed to having to re-visit every time they are potentially affected).
39-
* That does mean that this implementation will in exchange potentially visit intermediate nodes which do not actually
40-
* have any effects attached to them any more (hence it only being "semi lazy").
39+
* That does mean that this implementation will in exchange potentially visit intermediate nodes which, by the time
40+
* they're being visited, do not actually have any effects attached to them any more (hence it only being "semi lazy").
4141
* However, in practice, non-affected nodes usually vastly outnumber dead intermediate nodes (especially because
4242
* those are usually garbage collected together with the respective effects that used them) by one to two orders of
4343
* magnitude, making this well worth it.
44+
*
45+
* Furthermore, support has later been added to dynamically unregister such intermediate nodes which no longer have
46+
* any effects attached to them, which almost completely eliminates the above downside, at the cost of making
47+
* `getUntracked` more expensive on such nodes when called frequently, and a bit more internal bookkeeping.
4448
*/
4549
internal object MarkThenPushAndPullImpl : Impl {
4650
override fun <T> mutableState(value: T): MutableState<T> {
@@ -132,6 +136,40 @@ private class Node<T>(
132136
private val dependents: Sequence<Node<*>>
133137
get() = allDependents.asSequence().filterNot { it.suspended }.mapNotNull { it.dependent }
134138

139+
/**
140+
* Tracks how many of [allDependents] have [NodeKind.Effect] nodes attached to them (directly or transitively).
141+
* A number of 0 indicates that there are no [NodeKind.Effect] nodes observing this value in any way, so we don't
142+
* actually need to update it when one of its dependencies changes (we can defer that update until someone actually
143+
* wants the value).
144+
*
145+
* This intentionally uses [allDependents] rather than [dependents] and ignores the [Edge.suspended] flag for the
146+
* same reason that flag exists in the first place: When re-evaluating a node, we don't want to have to decrement
147+
* this counter on all its dependencies (and potentially their transitive dependencies), only to then likely have
148+
* to increment it again on most of them as the node re-subscribes to the same nodes again.
149+
* Instead we'll only update this counter when we also update the actual [allDependents] list.
150+
*/
151+
private var dependentsWithEffects = if (kind == NodeKind.Effect) 1 else 0
152+
set(value) {
153+
if (field == 0 && value > 0) {
154+
for (edge in allDependencies) {
155+
edge.dependency.dependentsWithEffects++
156+
}
157+
} else if (field > 0 && value == 0) {
158+
for (edge in allDependencies) {
159+
edge.dependency.dependentsWithEffects--
160+
}
161+
// Note: We must not yet unregister our dependency edges here!
162+
// For one, that'll likely result in a ConcurrentModificationException,
163+
// but more importantly, if we do that, then we won't know whether our value is up-to-date any more.
164+
// One of our dependencies could change, and we wouldn't know about it. So effectively we'd have
165+
// to re-compute all such effects on every call to `getUntracked`, which would be very expensive.
166+
// Instead we'll stay subscribed even when there's no downstream `effect` any more, and only
167+
// unsubscribe once we know we're Dirty.
168+
// This is handled in [markDirty].
169+
}
170+
field = value
171+
}
172+
135173
override fun Observer.get(): T {
136174
return getTracked(this@get)
137175
}
@@ -165,6 +203,10 @@ private class Node<T>(
165203
dependency.allDependents.add(edge)
166204
dependent.allDependencies.add(edge)
167205

206+
if (dependent.dependentsWithEffects > 0) {
207+
dependency.dependentsWithEffects++
208+
}
209+
168210
// To prevent unbounded growth, we'll clean up any stale edges whenever we add a new one
169211
// (this is really fast in when there isn't anything to do thanks to the ReferenceQueue)
170212
cleanupStaleReferences()
@@ -207,7 +249,28 @@ private class Node<T>(
207249
NodeState.Dirty, NodeState.Dead -> return // already dirty, nothing to do
208250
}
209251

210-
update.queueNode(this)
252+
// The node is dirty. If any effect is observing it, it needs to be updated.
253+
if (dependentsWithEffects > 0) {
254+
update.queueNode(this)
255+
} else {
256+
// otherwise we can defer updating until someone actually needs its value.
257+
258+
// We can now also dispose of all our dependency edges.
259+
// The only thing our dependencies could do with those edges is try to mark us as ToBeChecked or Dirty,
260+
// but we're already Dirty, so that wouldn't change anything.
261+
// And by disposing of these edges, we reduce the amount of downstream dependents which our dependencies
262+
// have to mark on every change.
263+
// See also the `Note` comment in the implementation of [dependentsWithEffects].
264+
for (edge in allDependencies) {
265+
// We cannot remove the edge from `edge.allDependents` directly, because the caller is likely iterating
266+
// over that list right now.
267+
// We can mark it as garbage collected though, and let the existing stale edge cleanup code deal with
268+
// it automatically.
269+
edge.clear() // sets reference value to `null`, so the edge appears as stale
270+
edge.enqueue() // enqueues edge in its ReferenceQueue, so cleanupStaleReferences will do something
271+
}
272+
allDependencies.clear()
273+
}
211274

212275
state = NodeState.Dirty
213276
}
@@ -254,6 +317,9 @@ private class Node<T>(
254317
allDependencies.removeIf { edge ->
255318
if (edge.suspended) {
256319
edge.dependency.allDependents.remove(edge)
320+
if (dependentsWithEffects > 0) {
321+
edge.dependency.dependentsWithEffects--
322+
}
257323
true
258324
} else {
259325
false
@@ -276,6 +342,7 @@ private class Node<T>(
276342

277343
for (edge in allDependencies) {
278344
edge.dependency.allDependents.remove(edge)
345+
edge.dependency.dependentsWithEffects--
279346
}
280347
allDependencies.clear()
281348

@@ -296,7 +363,19 @@ private class Node<T>(
296363
@Suppress("ControlFlowWithEmptyBody")
297364
while (queue.poll() != null);
298365

299-
allDependents.removeIf { it.dependent == null }
366+
var updatedEffectsCount = 0
367+
allDependents.removeIf { edge ->
368+
val dependent = edge.dependent
369+
if (dependent == null) {
370+
true
371+
} else {
372+
if (dependent.dependentsWithEffects > 0) {
373+
updatedEffectsCount += 1
374+
}
375+
false
376+
}
377+
}
378+
dependentsWithEffects = updatedEffectsCount
300379
}
301380

302381
/**

elementa/statev2/src/main/kotlin/gg/essential/gui/elementa/state/v2/time.kt

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,7 @@ class StateScheduler(val time: State<Instant>) {
7070
var trigger = triggerCache[bitsMatching]
7171
if (trigger == null || trigger.first != nextWakeupTime) {
7272
trigger = Pair(nextWakeupTime, memo {
73-
// FIXME States are currently not un-registered when they no longer have any subscribers, only once
74-
// garbage collections deletes them, this is usually good enough but here (and especially during tests)
75-
// it can result in a lot more than 64 states being registered to the root time source state. To avoid
76-
// that, we skip subscribing to the root time source state altogether when our target time has been
77-
// reached, thereby explicitly removing the subscription of this memo from the root time source state.
78-
// Should the State implementation ever be optimized to handle this itself, this can be simplified to:
79-
// timeSource() >= nextWakeupTime
80-
if (time.getUntracked() >= nextWakeupTime) {
81-
true
82-
} else {
83-
time()
84-
false
85-
}
73+
time() >= nextWakeupTime
8674
}.let {
8775
// FIXME while a single `memo` is functionality sufficient to decouple evaluation of the subscribers
8876
// from the time source State, a second `memo` is required due to current implementation details of

elementa/statev2/src/test/kotlin/gg/essential/gui/elementa/state/v2/MemoTest.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,14 @@ class MemoTest {
4646
assertEquals(1, ran, "memo result should be memoized")
4747

4848
state.set(1)
49-
// Ideally we wouldn't run the computation if no one is subscribed, current implementation does though
50-
// assertEquals(1, ran, "memo should not be updated if no one is subscribed")
49+
assertEquals(1, ran, "memo should not be updated if no one is subscribed")
5150
assertEquals(2, memoState.getUntracked())
5251
assertEquals(2, ran, "memo should only be evaluated once")
5352
assertEquals(2, memoState.getUntracked())
5453
assertEquals(2, ran, "memo result should be memoized")
5554

5655
state.set(2)
57-
// Ideally we wouldn't run the computation if no one is subscribed, current implementation does though
58-
// assertEquals(2, ran, "memo should not be updated if no one is subscribed")
56+
assertEquals(2, ran, "memo should not be updated if no one is subscribed")
5957
assertEquals(3, memoState.getUntracked())
6058
assertEquals(3, ran, "memo should only be evaluated once")
6159
assertEquals(3, memoState.getUntracked())

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ minecraftVersion=11202
1111
# TODO remove once upgrading to Loom 1.10
1212
# fabric-api 1.21.5 was built with Loom 1.10, seems to work well enough in dev with our current 1.7 though
1313
loom.ignoreDependencyLoomVersionValidation=true
14-
version=1.4.0.3
14+
version=1.4.1

0 commit comments

Comments
 (0)