diff --git a/build.gradle.kts b/build.gradle.kts index f9b16539..5a0131ed 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -174,14 +174,14 @@ modrinth { projectId.set("recode") versionNumber.set(modVersionWithMeta) - val match = Regex("""-(beta|alpha)\.""").find(modVersion) + val match = Regex("""-(?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 @@ -216,7 +216,7 @@ data class DependencyMod( * @return The list of [DependencyMod] values matching [type] in gradle.properties. */ fun dependencyModsOfType(type: String): List { - 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] diff --git a/src/main/java/io/github/homchom/recode/RecodeDispatcher.kt b/src/main/java/io/github/homchom/recode/RecodeDispatcher.kt index 8fc7915f..fea9be42 100644 --- a/src/main/java/io/github/homchom/recode/RecodeDispatcher.kt +++ b/src/main/java/io/github/homchom/recode/RecodeDispatcher.kt @@ -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 @@ -25,7 +25,7 @@ inline fun runOnMinecraftThread(crossinline block: () -> R) = */ object RecodeDispatcher : CoroutineContext { private val pending = ConcurrentLinkedQueue() - private val dispatcher = DerivedDispatcher(pending::add, mc::isSameThread) + private val dispatcher = YieldingExecutorDispatcher(pending::add, mc::isSameThread) private val delegate = dispatcher + CoroutineExceptionHandler { _, exception -> mc.sendSystemToast( diff --git a/src/main/java/io/github/homchom/recode/event/trial/DetectorImpl.kt b/src/main/java/io/github/homchom/recode/event/trial/DetectorImpl.kt index db7c3a6e..45a7cf30 100644 --- a/src/main/java/io/github/homchom/recode/event/trial/DetectorImpl.kt +++ b/src/main/java/io/github/homchom/recode/event/trial/DetectorImpl.kt @@ -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 @@ -120,13 +120,13 @@ private open class TrialDetector( 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( diff --git a/src/main/java/io/github/homchom/recode/event/trial/ToggleRequesterGroup.kt b/src/main/java/io/github/homchom/recode/event/trial/ToggleRequesterGroup.kt index 93ad1143..1e6610a5 100644 --- a/src/main/java/io/github/homchom/recode/event/trial/ToggleRequesterGroup.kt +++ b/src/main/java/io/github/homchom/recode/event/trial/ToggleRequesterGroup.kt @@ -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 diff --git a/src/main/java/io/github/homchom/recode/event/trial/Trial.kt b/src/main/java/io/github/homchom/recode/event/trial/Trial.kt index f34868a2..68e7b147 100644 --- a/src/main/java/io/github/homchom/recode/event/trial/Trial.kt +++ b/src/main/java/io/github/homchom/recode/event/trial/Trial.kt @@ -142,17 +142,7 @@ class TrialResult private constructor(private val deferred: Deferred 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 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. * @@ -23,33 +58,23 @@ import kotlin.time.Duration * 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 Flow.add() = buffer(Channel.UNLIMITED).produceIn(coroutineScope) + fun Flow.add(): ReceiveChannel /** * Transfers this context flow eagerly into a concurrent [Queue], allowing it to be used by the Trial if @@ -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 Flow.addOptional(): Queue { - val queue = ConcurrentLinkedQueue() - coroutineScope.launch { - collect { queue += it } - } - return queue - } + fun Flow.addOptional(): Queue /** * @see Flow.add @@ -78,12 +97,9 @@ class TrialScope @DelicateCoroutinesApi constructor( fun Listenable.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. @@ -93,36 +109,29 @@ class TrialScope @DelicateCoroutinesApi constructor( * * @see add */ - suspend inline fun test( + suspend fun test( channel: ReceiveChannel, 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 enforce( + suspend fun enforce( channel: ReceiveChannel, 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. @@ -139,11 +148,11 @@ class TrialScope @DelicateCoroutinesApi constructor( * * @see test */ - suspend inline fun testBoolean( + suspend fun testBoolean( channel: ReceiveChannel, attempts: UInt = 1u, timeoutDuration: Duration = DEFAULT_TIMEOUT_DURATION, - crossinline test: (C) -> Boolean + test: (C) -> Boolean ): Unit? { return test(channel, attempts, timeoutDuration) { test(it).unitOrNull() } } @@ -151,23 +160,22 @@ class TrialScope @DelicateCoroutinesApi constructor( /** * @see failOn */ - suspend inline fun enforceBoolean(channel: ReceiveChannel, crossinline test: (C) -> Boolean) { + suspend fun enforceBoolean(channel: ReceiveChannel, test: (C) -> Boolean) { enforce(channel) { test(it).unitOrNull() } } /** * @return an instant [TrialResult] with [value]. Use this when a trial does not end asynchronously. */ - fun instant(value: R?) = TrialResult(value) + fun instant(value: R?): TrialResult /** * @return the asynchronous [TrialResult] of [block] ran in its own [TrialScope]. */ - fun suspending(block: suspend TrialScope.() -> R?) = - TrialResult(block, coroutineScope, hidden) + fun suspending(block: suspend TrialScope.() -> R?): TrialResult /** - * A shorthand for `unitOrNull().let(::instant)`. + * A shortcut for `unitOrNull().let(::instant)`. * * @see instant * @see unitOrNull @@ -175,6 +183,44 @@ class TrialScope @DelicateCoroutinesApi constructor( 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 Flow.add() = buffer(Channel.UNLIMITED).produceIn(coroutineScope) + + override fun Flow.addOptional(): Queue { + val queue = ConcurrentLinkedQueue() + coroutineScope.launch { + collect { queue += it } + } + return queue + } + + override fun enforce(rule: () -> Unit) { + rule() + rules += rule + } + + override suspend fun enforce( + channel: ReceiveChannel, + coroutineContext: CoroutineContext, + test: (C) -> T? + ) { + coroutineScope.launch(coroutineContext, CoroutineStart.UNDISPATCHED) { + channel.consumeEach { test(it) ?: throw TrialScopeException() } + } + yield() // fast-fail + } + + override fun instant(value: R?) = TrialResult(value) + + override fun 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. diff --git a/src/main/java/io/github/homchom/recode/hypercube/state/DFStateDetectors.kt b/src/main/java/io/github/homchom/recode/hypercube/state/DFStateDetectors.kt index 74252c66..e5b66d2a 100644 --- a/src/main/java/io/github/homchom/recode/hypercube/state/DFStateDetectors.kt +++ b/src/main/java/io/github/homchom/recode/hypercube/state/DFStateDetectors.kt @@ -78,6 +78,7 @@ object DFStateDetectors : StateListenable> by eventGroup { } }, trial(JoinDFDetector, Unit) { info, _ -> + enforceOnDF() suspending { val permissions = power.async { val message = StateMessages.Profile.request(mc.player!!.username, true) diff --git a/src/main/java/io/github/homchom/recode/render/RenderEvents.kt b/src/main/java/io/github/homchom/recode/render/RenderEvents.kt index 60566728..aff2d34a 100644 --- a/src/main/java/io/github/homchom/recode/render/RenderEvents.kt +++ b/src/main/java/io/github/homchom/recode/render/RenderEvents.kt @@ -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 diff --git a/src/main/java/io/github/homchom/recode/ui/text/MiniMessageHighlighter.kt b/src/main/java/io/github/homchom/recode/ui/text/MiniMessageHighlighter.kt index c6bfaed6..738519df 100644 --- a/src/main/java/io/github/homchom/recode/ui/text/MiniMessageHighlighter.kt +++ b/src/main/java/io/github/homchom/recode/ui/text/MiniMessageHighlighter.kt @@ -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 diff --git a/src/main/java/io/github/homchom/recode/ui/text/TextFunctions.kt b/src/main/java/io/github/homchom/recode/ui/text/TextFunctions.kt index 7e844aa0..5ca07976 100644 --- a/src/main/java/io/github/homchom/recode/ui/text/TextFunctions.kt +++ b/src/main/java/io/github/homchom/recode/ui/text/TextFunctions.kt @@ -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 diff --git a/src/main/java/io/github/homchom/recode/util/coroutines/Continuations.kt b/src/main/java/io/github/homchom/recode/util/coroutines/Continuations.kt index 6c51e957..1e1da164 100644 --- a/src/main/java/io/github/homchom/recode/util/coroutines/Continuations.kt +++ b/src/main/java/io/github/homchom/recode/util/coroutines/Continuations.kt @@ -1,5 +1,6 @@ package io.github.homchom.recode.util.coroutines +import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.Continuation import kotlin.coroutines.ContinuationInterceptor @@ -7,8 +8,8 @@ import kotlin.coroutines.ContinuationInterceptor * Returns a new [ContinuationInterceptor] that invokes [onResume] before each successful resumption. */ // we don't optimize with Delay because CancellableContinuation is very internal -fun ContinuationInterceptor.withNotifications(onResume: () -> Unit) = - object : ContinuationInterceptor by this { +fun ContinuationInterceptor.withNotifications(onResume: () -> Unit): ContinuationInterceptor = + object : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { override fun interceptContinuation(continuation: Continuation) = Continuation(continuation.context) { result -> if (result.isSuccess) onResume() diff --git a/src/main/java/io/github/homchom/recode/util/lib/CoroutinesFunctions.kt b/src/main/java/io/github/homchom/recode/util/coroutines/CoroutinesFunctions.kt similarity index 88% rename from src/main/java/io/github/homchom/recode/util/lib/CoroutinesFunctions.kt rename to src/main/java/io/github/homchom/recode/util/coroutines/CoroutinesFunctions.kt index 14f15b37..5159b3a0 100644 --- a/src/main/java/io/github/homchom/recode/util/lib/CoroutinesFunctions.kt +++ b/src/main/java/io/github/homchom/recode/util/coroutines/CoroutinesFunctions.kt @@ -1,4 +1,4 @@ -package io.github.homchom.recode.util.lib +package io.github.homchom.recode.util.coroutines import kotlinx.coroutines.* import kotlin.coroutines.EmptyCoroutineContext diff --git a/src/main/java/io/github/homchom/recode/util/coroutines/DerivedDispatcher.kt b/src/main/java/io/github/homchom/recode/util/coroutines/DerivedDispatcher.kt deleted file mode 100644 index 144d2370..00000000 --- a/src/main/java/io/github/homchom/recode/util/coroutines/DerivedDispatcher.kt +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.homchom.recode.util.coroutines - -import kotlinx.coroutines.* -import java.util.concurrent.Executor -import kotlin.coroutines.CoroutineContext - -inline fun DerivedDispatcher(executor: Executor, crossinline immediatePredicate: () -> Boolean) = - DerivedDispatcher( - { block -> - if (immediatePredicate()) block.run() else executor.execute(block) - }, - executor - ) - -fun DerivedDispatcher(executor: Executor) = DerivedDispatcher(executor) { false } - -@OptIn(InternalCoroutinesApi::class) -class DerivedDispatcher private constructor( - private val defaultDelegate: CoroutineDispatcher, - private val yieldingDelegate: CoroutineDispatcher -) : CoroutineDispatcher(), Delay { - private val delay get() = defaultDelegate as Delay - - constructor(defaultExecutor: Executor, yieldingExecutor: Executor) : this( - defaultExecutor.asCoroutineDispatcher(), - yieldingExecutor.asCoroutineDispatcher() - ) - - constructor(dispatcher: DerivedDispatcher) : this( - dispatcher.defaultDelegate, - dispatcher.yieldingDelegate - ) - - override fun dispatch(context: CoroutineContext, block: Runnable) = - defaultDelegate.dispatch(context, block) - - override fun dispatchYield(context: CoroutineContext, block: Runnable) = - yieldingDelegate.dispatchYield(context, block) - - override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = - delay.scheduleResumeAfterDelay(timeMillis, continuation) - - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext) = - delay.invokeOnTimeout(timeMillis, block, context) -} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/util/coroutines/YieldingExecutorDispatcher.kt b/src/main/java/io/github/homchom/recode/util/coroutines/YieldingExecutorDispatcher.kt new file mode 100644 index 00000000..7e9af40e --- /dev/null +++ b/src/main/java/io/github/homchom/recode/util/coroutines/YieldingExecutorDispatcher.kt @@ -0,0 +1,52 @@ +package io.github.homchom.recode.util.coroutines + +import kotlinx.coroutines.* +import java.util.concurrent.Executor +import kotlin.coroutines.CoroutineContext + +/** + * Constructs a [YieldingExecutorDispatcher] with a shared (yielding) [executor], using [immediatePredicate] + * to determine whether dispatched [Runnable] blocks should be executed immediately. + */ +inline fun YieldingExecutorDispatcher(executor: Executor, crossinline immediatePredicate: () -> Boolean) = + YieldingExecutorDispatcher( + { block -> + if (immediatePredicate()) block.run() else executor.execute(block) + }, + executor + ) + +/** + * Constructs a [YieldingExecutorDispatcher] with a shared [executor] that should always yield. + */ +fun YieldingExecutorDispatcher(executor: Executor) = YieldingExecutorDispatcher(executor) { false } + +/** + * A [CoroutineDispatcher] derived from two [Executor]s to support [yield]ing. + * + * @param defaultExecutor The executor used by [dispatch]. + * @param yieldingExecutor The executor used by [dispatchYield]. + * + * @see asCoroutineDispatcher + */ +@OptIn(InternalCoroutinesApi::class) +class YieldingExecutorDispatcher( + defaultExecutor: Executor, + yieldingExecutor: Executor +) : CoroutineDispatcher(), Delay { + private val defaultDelegate = defaultExecutor.asCoroutineDispatcher() + private val yieldingDelegate = yieldingExecutor.asCoroutineDispatcher() + private val delay get() = defaultDelegate as Delay + + override fun dispatch(context: CoroutineContext, block: Runnable) = + defaultDelegate.dispatch(context, block) + + override fun dispatchYield(context: CoroutineContext, block: Runnable) = + yieldingDelegate.dispatchYield(context, block) + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = + delay.scheduleResumeAfterDelay(timeMillis, continuation) + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext) = + delay.invokeOnTimeout(timeMillis, block, context) +} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/util/lib/CollectionsFunctions.kt b/src/main/java/io/github/homchom/recode/util/std/CollectionsFunctions.kt similarity index 84% rename from src/main/java/io/github/homchom/recode/util/lib/CollectionsFunctions.kt rename to src/main/java/io/github/homchom/recode/util/std/CollectionsFunctions.kt index 21605091..82c48a6b 100644 --- a/src/main/java/io/github/homchom/recode/util/lib/CollectionsFunctions.kt +++ b/src/main/java/io/github/homchom/recode/util/std/CollectionsFunctions.kt @@ -1,4 +1,4 @@ -package io.github.homchom.recode.util.lib +package io.github.homchom.recode.util.std /** * Maps this list into an [Array]. diff --git a/src/main/java/io/github/homchom/recode/util/lib/StdlibFunctions.kt b/src/main/java/io/github/homchom/recode/util/std/StdlibFunctions.kt similarity index 92% rename from src/main/java/io/github/homchom/recode/util/lib/StdlibFunctions.kt rename to src/main/java/io/github/homchom/recode/util/std/StdlibFunctions.kt index cdb653d2..6323f5d5 100644 --- a/src/main/java/io/github/homchom/recode/util/lib/StdlibFunctions.kt +++ b/src/main/java/io/github/homchom/recode/util/std/StdlibFunctions.kt @@ -1,6 +1,6 @@ @file:JvmName("BasicTypeExtensions") -package io.github.homchom.recode.util.lib +package io.github.homchom.recode.util.std // booleans