Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,23 @@ fun ClientGameTestContext.testComposeScreenMeasuresRenderableNode() {
See `ComposeRenderingTests.kt` for a full example. Registered the same way, via `register<TestClass>()`
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -154,16 +157,34 @@ 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<FluidResource>? = 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(
encoder: Encoder,
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)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ArchieFluidSlot> = 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)
}
}
}
}
Loading