Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

Commit

Permalink
enforce on steroids, cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
homchom committed Jan 5, 2024
1 parent 0b05f06 commit 1fd7c24
Show file tree
Hide file tree
Showing 16 changed files with 175 additions and 130 deletions.
10 changes: 5 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,14 @@ modrinth {
projectId.set("recode")
versionNumber.set(modVersionWithMeta)

val match = Regex("""-(beta|alpha)\.""").find(modVersion)
val match = Regex("""-(?<phase>beta|alpha)\.""").find(modVersion)
if (match == null) {
versionName.set(modVersion)
versionType.set("release")
} else {
val type = match.groupValues[1]
versionName.set(modVersion.replaceRange(match.range, " $type "))
versionType.set(type)
val phase = match.groups["phase"]!!.value
versionName.set(modVersion.replaceRange(match.range, " $phase "))
versionType.set(phase)
}

// remove "LATEST" classifiers when uploading to modrinth
Expand Down Expand Up @@ -216,7 +216,7 @@ data class DependencyMod(
* @return The list of [DependencyMod] values matching [type] in gradle.properties.
*/
fun dependencyModsOfType(type: String): List<DependencyMod> {
val regex = Regex("""$type\.([a-z][a-z0-9-_]{1,63})\.artifact""")
val regex = Regex("""$type\.([^\.]+)\.artifact""")
return properties.mapNotNull { (key, value) ->
regex.matchEntire(key)?.let { match ->
val id = match.groupValues[1]
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/github/homchom/recode/RecodeDispatcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package io.github.homchom.recode

import io.github.homchom.recode.ui.sendSystemToast
import io.github.homchom.recode.ui.text.translatedText
import io.github.homchom.recode.util.coroutines.DerivedDispatcher
import io.github.homchom.recode.util.coroutines.YieldingExecutorDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Runnable
Expand All @@ -25,7 +25,7 @@ inline fun <R> runOnMinecraftThread(crossinline block: () -> R) =
*/
object RecodeDispatcher : CoroutineContext {
private val pending = ConcurrentLinkedQueue<Runnable>()
private val dispatcher = DerivedDispatcher(pending::add, mc::isSameThread)
private val dispatcher = YieldingExecutorDispatcher(pending::add, mc::isSameThread)

private val delegate = dispatcher + CoroutineExceptionHandler { _, exception ->
mc.sendSystemToast(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import io.github.homchom.recode.event.Requester
import io.github.homchom.recode.event.createEvent
import io.github.homchom.recode.ui.sendSystemToast
import io.github.homchom.recode.ui.text.translatedText
import io.github.homchom.recode.util.lib.lazyJob
import io.github.homchom.recode.util.coroutines.lazyJob
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
Expand Down Expand Up @@ -120,13 +120,13 @@ private open class TrialDetector<T, R : Any>(
successful: AtomicBoolean
) {
val entryScope = CoroutineScope(power.coroutineContext + lazyJob())

val result = try {
val trialScope = TrialScope(entryScope, entry?.hidden ?: false)
val result = entryScope.nonSuspendingTrialScope(entry?.hidden ?: false) {
logDebug { "trial $trialIndex started for ${debugString(entry?.input, entry?.hidden)}" }
supplier.supplyIn(trialScope, entry?.input, entry?.isRequest ?: false)
} catch (e: TrialScopeException) {
null
supplier.supplyIn(
this,
entry?.input,
entry?.isRequest ?: false
)
}

fun finish(state: String) = entryScope.cancelAndLog(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.github.homchom.recode.event.trial
import io.github.homchom.recode.Power
import io.github.homchom.recode.event.Listenable
import io.github.homchom.recode.event.Requester
import io.github.homchom.recode.util.lib.unitOrNull
import io.github.homchom.recode.util.std.unitOrNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

Expand Down
12 changes: 1 addition & 11 deletions src/main/java/io/github/homchom/recode/event/trial/Trial.kt
Original file line number Diff line number Diff line change
Expand Up @@ -142,17 +142,7 @@ class TrialResult<T : Any> private constructor(private val deferred: Deferred<T?
scope: CoroutineScope,
hidden: Boolean = false
) : this(
scope.async {
try {
coroutineScope {
val trialScope = TrialScope(this, hidden)
yield()
trialScope.asyncBlock().also { coroutineContext.cancelChildren() }
}
} catch (e: TrialScopeException) {
null
}
}
scope.async { suspendingTrialScope(hidden, asyncBlock) }
)
}

Expand Down
148 changes: 97 additions & 51 deletions src/main/java/io/github/homchom/recode/event/trial/TrialScope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package io.github.homchom.recode.event.trial

import io.github.homchom.recode.DEFAULT_TIMEOUT_DURATION
import io.github.homchom.recode.event.Listenable
import io.github.homchom.recode.util.lib.unitOrNull
import io.github.homchom.recode.util.coroutines.withNotifications
import io.github.homchom.recode.util.std.unitOrNull
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
Expand All @@ -12,44 +13,68 @@ import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.produceIn
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.time.Duration

/**
* Runs a *non-suspending* [block] in a [TrialScope]. This is more internal than [trial] and
* should not be called from general code.
*/
@DelicateCoroutinesApi
fun <R : Any> CoroutineScope.nonSuspendingTrialScope(hidden: Boolean, block: TrialScope.() -> R?) =
try {
TrialCoroutineScope(this, hidden).block()
} catch (e: TrialScopeException) {
null
}

/**
* Runs a *suspending* [block] in a [TrialScope]. This is more internal than [trial] and
* should not be called from general code.
*/
@DelicateCoroutinesApi
suspend fun <R : Any> suspendingTrialScope(hidden: Boolean, block: suspend TrialScope.() -> R?) =
try {
coroutineScope {
val trialScope = TrialCoroutineScope(this, hidden)
val interceptor = coroutineContext[ContinuationInterceptor]!!
.withNotifications { for (rule in trialScope.rules) rule() }

yield() // give channels at least one opportunity to send

withContext(interceptor) { trialScope.block() }
.also { coroutineContext.cancelChildren() }
}
} catch (e: TrialScopeException) {
null
}

/**
* A [CoroutineScope] that a [Trial] executes in.
*
* A trial is a test containing one or more suspension points on events; they are useful for detecting
* an occurrence that happens in complex steps. TrialScope includes corresponding DSL functions such as
* [add] and [test].
*
* @param hidden To be used by trials to invalidate "notification-like" intermediate event contexts.
* @property hidden To be used by trials to invalidate "notification-like" intermediate event contexts.
*
* @see trial
*/
class TrialScope @DelicateCoroutinesApi constructor(
val coroutineScope: CoroutineScope,
val hidden: Boolean = false,
) {
/**
* A list of blocking rules that are tested after most trial suspensions, failing the trial on a failed test.
*
* @see test
*/
val rules: List<() -> Unit> get() = _rules

private val _rules = mutableListOf<() -> Unit>()
sealed interface TrialScope {
val hidden: Boolean

/**
* An alias for [UInt.MAX_VALUE], used when a test should run as long as possible (in an awaiting fashion).
*/
inline val unlimited get() = UInt.MAX_VALUE
val unlimited get() = UInt.MAX_VALUE

/**
* Transfers this context flow eagerly into a [kotlinx.coroutines.channels.Channel], allowing it to be used
* by the Trial in a suspending manner.
*/
fun <T> Flow<T>.add() = buffer(Channel.UNLIMITED).produceIn(coroutineScope)
fun <T> Flow<T>.add(): ReceiveChannel<T>

/**
* Transfers this context flow eagerly into a concurrent [Queue], allowing it to be used by the Trial if
Expand All @@ -59,13 +84,7 @@ class TrialScope @DelicateCoroutinesApi constructor(
* with nullable context is desired for use, map it into a flow of [io.github.homchom.recode.util.Case]
* objects before calling.
*/
fun <T : Any> Flow<T>.addOptional(): Queue<T> {
val queue = ConcurrentLinkedQueue<T>()
coroutineScope.launch {
collect { queue += it }
}
return queue
}
fun <T : Any> Flow<T>.addOptional(): Queue<T>

/**
* @see Flow.add
Expand All @@ -78,12 +97,9 @@ class TrialScope @DelicateCoroutinesApi constructor(
fun <T : Any> Listenable<T>.addOptional() = notifications.addOptional()

/**
* Enforces [rule] by adding it to [rules].
* Enforces [rule] by invoking it after every suspension point.
*/
fun enforce(rule: () -> Unit) {
rule()
_rules += rule
}
fun enforce(rule: () -> Unit)

/**
* Tests [test] on the first [attempts] values of [channel] until a non-null result is returned.
Expand All @@ -93,36 +109,29 @@ class TrialScope @DelicateCoroutinesApi constructor(
*
* @see add
*/
suspend inline fun <C, T : Any> test(
suspend fun <C, T : Any> test(
channel: ReceiveChannel<C>,
attempts: UInt = 1u,
timeoutDuration: Duration = DEFAULT_TIMEOUT_DURATION,
crossinline test: suspend (C) -> T?
test: suspend (C) -> T?
): T? {
val result = withTimeoutOrNull(timeoutDuration) {
return withTimeoutOrNull(timeoutDuration) {
(1u..attempts).firstNotNullOfOrNull { test(channel.receive()) }
}
for (rule in rules) rule()
return result
}

/**
* Asynchronously enforces [test] on the remaining elements of [channel], consuming the channel and failing
* the trial on a failed test. Also yields (suspends) for one iteration of Minecraft's event loop so [channel]
* is up-to-date.
* Asynchronously enforces [test] on the remaining elements of [channel], consuming the channel and
* failing the trial on a failed test. Also yields (suspends) for one iteration of Minecraft's event
* loop so [channel] is up-to-date.
*
* @see TrialScope.enforce
*/
suspend inline fun <C, T : Any> enforce(
suspend fun <C, T : Any> enforce(
channel: ReceiveChannel<C>,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
crossinline test: (C) -> T?
) {
coroutineScope.launch(coroutineContext, CoroutineStart.UNDISPATCHED) {
channel.consumeEach { test(it) ?: throw TrialScopeException() }
}
yield() // fast-fail
}
test: (C) -> T?
)

/**
* Fails the trial if any elements in [channel] are received.
Expand All @@ -139,42 +148,79 @@ class TrialScope @DelicateCoroutinesApi constructor(
*
* @see test
*/
suspend inline fun <C> testBoolean(
suspend fun <C> testBoolean(
channel: ReceiveChannel<C>,
attempts: UInt = 1u,
timeoutDuration: Duration = DEFAULT_TIMEOUT_DURATION,
crossinline test: (C) -> Boolean
test: (C) -> Boolean
): Unit? {
return test(channel, attempts, timeoutDuration) { test(it).unitOrNull() }
}

/**
* @see failOn
*/
suspend inline fun <C> enforceBoolean(channel: ReceiveChannel<C>, crossinline test: (C) -> Boolean) {
suspend fun <C> enforceBoolean(channel: ReceiveChannel<C>, test: (C) -> Boolean) {
enforce(channel) { test(it).unitOrNull() }
}

/**
* @return an instant [TrialResult] with [value]. Use this when a trial does not end asynchronously.
*/
fun <R : Any> instant(value: R?) = TrialResult(value)
fun <R : Any> instant(value: R?): TrialResult<R>

/**
* @return the asynchronous [TrialResult] of [block] ran in its own [TrialScope].
*/
fun <R : Any> suspending(block: suspend TrialScope.() -> R?) =
TrialResult(block, coroutineScope, hidden)
fun <R : Any> suspending(block: suspend TrialScope.() -> R?): TrialResult<R>

/**
* A shorthand for `unitOrNull().let(::instant)`.
* A shortcut for `unitOrNull().let(::instant)`.
*
* @see instant
* @see unitOrNull
*/
fun Boolean.instantUnitOrNull() = instant(unitOrNull())
}

private class TrialCoroutineScope(
private val coroutineScope: CoroutineScope,
override val hidden: Boolean = false
) : TrialScope, CoroutineScope by coroutineScope {
val rules = mutableListOf<() -> Unit>()

override fun <T> Flow<T>.add() = buffer(Channel.UNLIMITED).produceIn(coroutineScope)

override fun <T : Any> Flow<T>.addOptional(): Queue<T> {
val queue = ConcurrentLinkedQueue<T>()
coroutineScope.launch {
collect { queue += it }
}
return queue
}

override fun enforce(rule: () -> Unit) {
rule()
rules += rule
}

override suspend fun <C, T : Any> enforce(
channel: ReceiveChannel<C>,
coroutineContext: CoroutineContext,
test: (C) -> T?
) {
coroutineScope.launch(coroutineContext, CoroutineStart.UNDISPATCHED) {
channel.consumeEach { test(it) ?: throw TrialScopeException() }
}
yield() // fast-fail
}

override fun <R : Any> instant(value: R?) = TrialResult(value)

override fun <R : Any> suspending(block: suspend TrialScope.() -> R?) =
TrialResult(block, coroutineScope, hidden)
}

/**
* An exceptional return in a [TrialScope], such as when [TrialScope.enforce] fails. This can be safely thrown
* from inside the tests of a [trial], but is expensive; a `return` is almost always preferable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ object DFStateDetectors : StateListenable<Case<DFState?>> by eventGroup {
}
},
trial(JoinDFDetector, Unit) { info, _ ->
enforceOnDF()
suspending {
val permissions = power.async {
val message = StateMessages.Profile.request(mc.player!!.username, true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.github.homchom.recode.game.ticks
import io.github.homchom.recode.mc
import io.github.homchom.recode.util.Case
import io.github.homchom.recode.util.math.MixedInt
import io.github.homchom.recode.util.lib.mapToArray
import io.github.homchom.recode.util.std.mapToArray
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents.BeforeBlockOutline
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.homchom.recode.ui.text

import io.github.homchom.recode.util.lib.interpolate
import io.github.homchom.recode.util.std.interpolate
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.TextDecoration
import net.kyori.adventure.text.minimessage.Context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

package io.github.homchom.recode.ui.text

import io.github.homchom.recode.util.lib.fromCodePoint
import io.github.homchom.recode.util.std.fromCodePoint
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.Style
import net.minecraft.util.FormattedCharSequence
Expand Down
Loading

0 comments on commit 1fd7c24

Please sign in to comment.