diff --git a/AGENTS.md b/AGENTS.md index 5ad2bdc42..36e1b7532 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,6 +123,23 @@ fun ClientGameTestContext.testComposeScreenMeasuresRenderableNode() { See `ComposeRenderingTests.kt` for a full example. Registered the same way, via `register()` inside the `client { }` block. +**Known open issue - client GameTests are intermittently flaky (~1-in-5), cause not confirmed.** +Repeated local runs of `neoforge:runGametestClient` show a different click/animation-driven test +failing each time (`ConfirmDialog`, `RadioGroup`, ...) with `IllegalStateException: Predicate did +not become true within 200 ticks` and no logged exception - not a per-test logic bug. The specific +`isComposeIdle()` TOCTOU gap documented in `ComposeScreen.kt`'s KDoc is already fixed (it now uses +`Recomposer.hasPendingWork`, not the old `recomposeJob`-based check), so that's not the live cause. +The likely remaining gap: `ComposeScreen`'s coroutine scope (`CoroutineScope(Dispatchers.Default) + +BroadcastFrameClock`) runs on **real** threads/wall-clock time, so a composable's `delay(...)` (e.g. +`ConfirmDialog`'s close animation) genuinely races the harness's tick-based polling and the real +render loop's frame delivery. Real Jetpack Compose's own test tooling +(`ComposeTestRule`/`runComposeUiTest`) avoids this whole class of race by backing the composition +with a *virtual* clock/dispatcher (`TestMonotonicFrameClock` over `StandardTestDispatcher`) that +`waitForIdle()` drives forward deterministically, instead of polling real concurrency - Archie's +harness has no equivalent. A real fix likely means a test-only virtual-clock/dispatcher swap for +`ComposeScreen` during GameTests, not another polling tweak. Not yet attempted - would need live +instrumentation to confirm before changing anything. + ### Current test coverage - `ArchieItemHandlerTests` (`server`) – item storage/handler behavior - `BlockEntityNBTHolderTests` (`server`) – `NBTHolder` field defaults, save/load round-tripping, `@Sync` filtering diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt index 3b7fa3060..2f5e0fd94 100644 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt @@ -7,8 +7,11 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.serializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure import net.benwoodworth.knbt.NbtTag import net.kernelpanicsoft.archie.serialization.NBT import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless @@ -117,15 +120,31 @@ class ArchieEnergyStorage( override fun deserialize(decoder: Decoder): ArchieEnergyStorage { - val capacity = decoder.decodeLong().coerceAtLeast(0) - val amount = decoder.decodeLong().coerceIn(0, capacity) - return ArchieEnergyStorage(capacity).also { it.amount = amount } + return decoder.decodeStructure(descriptor) + { + var capacity = 0L + var amount = 0L + while (true) + { + when (val index = decodeElementIndex(descriptor)) + { + 0 -> capacity = decodeLongElement(descriptor, 0).coerceAtLeast(0) + 1 -> amount = decodeLongElement(descriptor, 1) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + ArchieEnergyStorage(capacity).also { it.amount = amount.coerceIn(0, capacity) } + } } override fun serialize(encoder: Encoder, value: ArchieEnergyStorage) { - encoder.encodeLong(value.capacity) - encoder.encodeLong(value.amount) + encoder.encodeStructure(descriptor) + { + encodeLongElement(descriptor, 0, value.capacity) + encodeLongElement(descriptor, 1, value.amount) + } } } } diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt index 287e3181f..1f6093941 100644 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt @@ -12,8 +12,11 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.descriptors.element import kotlinx.serialization.descriptors.nullable +import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure import net.benwoodworth.knbt.NbtTag import net.kernelpanicsoft.archie.serialization.NBT import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless @@ -154,7 +157,22 @@ class ArchieFluidSlot(private val limit: Long, private val onUpdate: () -> Unit override fun deserialize(decoder: Decoder): ArchieFluidSlot { - return ArchieFluidSlot(decoder.decodeLong(), surrogate.deserialize(decoder)) + return decoder.decodeStructure(descriptor) + { + var limit = 0L + var resourceStack: ResourceStack? = null + while (true) + { + when (val index = decodeElementIndex(descriptor)) + { + 0 -> limit = decodeLongElement(descriptor, 0) + 1 -> resourceStack = decodeNullableSerializableElement(descriptor, 1, surrogate) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + ArchieFluidSlot(limit, resourceStack ?: ResourceStack(FluidResource.BLANK, 0)) + } } override fun serialize( @@ -162,8 +180,11 @@ class ArchieFluidSlot(private val limit: Long, private val onUpdate: () -> Unit value: ArchieFluidSlot ) { - encoder.encodeLong(value.limit) - surrogate.serialize(encoder, value.resourceStack) + encoder.encodeStructure(descriptor) + { + encodeLongElement(descriptor, 0, value.limit) + encodeNullableSerializableElement(descriptor, 1, surrogate, value.resourceStack) + } } } } \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt index 7ad1199d5..b90779a55 100644 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt @@ -10,8 +10,11 @@ import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.serializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure import net.benwoodworth.knbt.NbtTag import net.kernelpanicsoft.archie.serialization.NBT import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless @@ -93,14 +96,31 @@ open class ArchieFluidStorage private constructor( override fun deserialize(decoder: Decoder): ArchieFluidStorage { - val limit = decoder.decodeLong() - return ArchieFluidStorage(limit, NonNullList.of(ArchieFluidSlot(limit), *surrogate.deserialize(decoder).toTypedArray())) + return decoder.decodeStructure(descriptor) + { + var limit = 0L + var slots: List = emptyList() + while (true) + { + when (val index = decodeElementIndex(descriptor)) + { + 0 -> limit = decodeLongElement(descriptor, 0) + 1 -> slots = decodeSerializableElement(descriptor, 1, surrogate) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + ArchieFluidStorage(limit, NonNullList.of(ArchieFluidSlot(limit), *slots.toTypedArray())) + } } override fun serialize(encoder: Encoder, value: ArchieFluidStorage) { - encoder.encodeLong(value.limit) - surrogate.serialize(encoder, value.slots) + encoder.encodeStructure(descriptor) + { + encodeLongElement(descriptor, 0, value.limit) + encodeSerializableElement(descriptor, 1, surrogate, value.slots) + } } } } \ No newline at end of file