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
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ fun GameTestHelper.testFieldDefaultsAndPersistenceRoundTrip() {
}
```

- **Assertions**: internal extension functions on `GameTestHelper` from `GameTestAssertions.kt`
(`.../gametest/internal/tests/GameTestAssertions.kt`), called unqualified inside a test method:
- **Assertions**: public extension functions on `GameTestHelper` from `GameTestAssertions.kt`
(`net.kernelpanicsoft.archie.gametest.GameTestAssertions.kt` — import them, they're not
same-package with your test class), usable by consuming mods too, not just Archie's own suite:
- `assertEquals(expected, actual)` – check equality, with an optional custom `message` lambda
- `assertTrue(condition) { message }` – check a boolean condition
- `expectThrows<ExceptionType> { block }` – assert `block` throws `ExceptionType`, returns the caught exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ internal fun AEvents.ArchieGameTestBuilder.archieTestGameTests()
client {
register<TestScreenGameTest>()
}
server {
register<CapabilityLookupTests>()
}
}

internal object ArchieTestGameTest : AGameTestEventObject(ArchieTest.MOD)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package net.kernelpanicsoft.archie.test.gametest

import net.kernelpanicsoft.archie.test.TileRegistry
import net.kernelpanicsoft.archie.transfer.exposeItemStorage
import net.minecraft.core.Direction

/**
* Exposes [TileRegistry.TestTile]'s existing `items` [net.kernelpanicsoft.archie.transfer.ArchieItemStorage]
* for [CapabilityLookupTests] to assert against.
*
* This can't live inside the `@GameTest` methods themselves: Common Storage Lib's
* `BlockLookup.onRegister` (what `exposeItemStorage` calls) is a one-shot listener CSL invokes
* during its own platform registration event (Fabric's `ItemApiLookup`/NeoForge's
* `RegisterCapabilitiesEvent`) - which has already fired long before a GameTest server finishes
* booting and starts ticking tests. [init] is called from [net.kernelpanicsoft.archie.test.ArchieTest.init],
* after `TileRegistry.init()` (so [TileRegistry.TestTile] actually exists) but still during normal
* mod initialization - the same point real mod code would call `exposeItemStorage` from.
*/
internal object CapabilityLookupTestFixtures
{
fun init()
{
TileRegistry.TestTile.exposeItemStorage { tile, direction ->
if (direction == Direction.DOWN) null else tile.items
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package net.kernelpanicsoft.archie.test.gametest

import earth.terrarium.common_storage_lib.item.ItemApi
import net.kernelpanicsoft.archie.gametest.assertEquals
import net.kernelpanicsoft.archie.gametest.assertTrue
import net.kernelpanicsoft.archie.gametest.internal.EMPTY
import net.kernelpanicsoft.archie.test.BlockRegistry
import net.kernelpanicsoft.archie.test.TestTile
import net.kernelpanicsoft.archie.test.TileRegistry
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.gametest.framework.GameTest
import net.minecraft.gametest.framework.GameTestHelper
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.entity.ChestBlockEntity

/**
* GameTest coverage for [net.kernelpanicsoft.archie.transfer.ArchieCapabilityExposure]: confirms
* `exposeItemStorage` (registered against [TileRegistry.TestTile] by [CapabilityLookupTestFixtures])
* actually reaches [ItemApi.BLOCK] (Common Storage Lib's real, platform-native lookup), not just
* some Archie-internal bookkeeping.
*/
@Suppress("unused")
class CapabilityLookupTests
{
@GameTest(template = EMPTY)
fun GameTestHelper.testExposeItemStorageReachesItemApiBlock()
{
val pos = BlockPos(10, 2, 3)
val state = BlockRegistry.TestBlock.defaultBlockState()
val tile = TestTile(pos, state)

val found = ItemApi.BLOCK.find(level, pos, state, tile, null)
assertTrue(found === tile.items) { "Expected TestTile.items to be reachable via ItemApi.BLOCK.find" }
succeed()
}

@GameTest(template = EMPTY)
fun GameTestHelper.testExposeItemStorageRespectsDirection()
{
val pos = BlockPos(11, 2, 3)
val state = BlockRegistry.TestBlock.defaultBlockState()
val tile = TestTile(pos, state)

assertTrue(ItemApi.BLOCK.find(level, pos, state, tile, Direction.UP) === tile.items) { "Expected UP to resolve TestTile.items" }
assertEquals(null, ItemApi.BLOCK.find(level, pos, state, tile, Direction.DOWN)) { "Expected DOWN to be excluded by the fixture's selector" }
succeed()
}

@GameTest(template = EMPTY)
fun GameTestHelper.testUnexposedTypeIsNotFound()
{
// Vanilla's own chest is never exposed anywhere in this suite.
val pos = BlockPos(12, 2, 3)
val state = Blocks.CHEST.defaultBlockState()
val blockEntity = ChestBlockEntity(pos, state)

assertEquals(null, ItemApi.BLOCK.find(level, pos, state, blockEntity, null)) { "Expected an unexposed type to resolve to null" }
succeed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform
import net.kernelpanicsoft.archie.events.AEvents
import net.kernelpanicsoft.archie.gametest.AGameTestPlatform
import net.kernelpanicsoft.archie.test.gametest.ArchieTestGameTest
import net.kernelpanicsoft.archie.test.gametest.CapabilityLookupTestFixtures
import net.kernelpanicsoft.archie.test.data.ArchieTestDatagen
import net.kernelpanicsoft.archie.util.onClient
import net.minecraft.resources.ResourceLocation
Expand Down Expand Up @@ -38,6 +39,11 @@ object ArchieTest
ItemRegistry.init()
TileRegistry.init()
GuiRegistry.init()
// Must come after TileRegistry.init() (needs TestTile to actually exist), but is otherwise
// still normal mod-init timing - see CapabilityLookupTestFixtures's KDoc for why this can't
// be deferred to inside the @GameTest methods themselves.
if (AGameTestPlatform.isGameTest)
CapabilityLookupTestFixtures.init()
}

@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package net.kernelpanicsoft.archie.gametest.internal.tests

import earth.terrarium.common_storage_lib.resources.item.ItemResource
import net.kernelpanicsoft.archie.gametest.assertEquals
import net.kernelpanicsoft.archie.gametest.assertTrue
import net.kernelpanicsoft.archie.gametest.internal.EMPTY
import net.kernelpanicsoft.archie.transfer.ArchieItemStorage
import net.minecraft.gametest.framework.GameTest
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package net.kernelpanicsoft.archie.gametest.internal.tests

import dev.architectury.fluid.FluidStack
import net.kernelpanicsoft.archie.gametest.assertEquals
import net.kernelpanicsoft.archie.gametest.assertTrue
import net.kernelpanicsoft.archie.gametest.internal.EMPTY
import net.kernelpanicsoft.archie.serialization.NBTHolder
import net.kernelpanicsoft.archie.serialization.Sync
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package net.kernelpanicsoft.archie.gametest.internal.tests

import kotlinx.serialization.builtins.serializer
import net.kernelpanicsoft.archie.gametest.assertEquals
import net.kernelpanicsoft.archie.gametest.assertTrue
import net.kernelpanicsoft.archie.gametest.internal.EMPTY
import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager
import net.minecraft.core.BlockPos
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package net.kernelpanicsoft.archie.gametest.internal.tests
package net.kernelpanicsoft.archie.gametest

import net.minecraft.gametest.framework.GameTestHelper

/**
* Fails this GameTest via [GameTestHelper.fail] with [message] if [condition] is `false`.
*/
internal fun GameTestHelper.assertTrue(condition: Boolean, message: () -> String)
fun GameTestHelper.assertTrue(condition: Boolean, message: () -> String)
{
if (!condition) {
fail(message())
Expand All @@ -17,7 +17,7 @@ internal fun GameTestHelper.assertTrue(condition: Boolean, message: () -> String
*
* @param message Failure message builder; defaults to reporting both values.
*/
internal fun <T> GameTestHelper.assertEquals(
fun <T> GameTestHelper.assertEquals(
expected: T,
actual: T,
message: () -> String = { "Expected <$expected>, got <$actual>" },
Expand All @@ -34,7 +34,7 @@ internal fun <T> GameTestHelper.assertEquals(
*
* @return The caught exception of type [T].
*/
internal inline fun <reified T : Throwable> GameTestHelper.expectThrows(noinline block: () -> Unit): T
inline fun <reified T : Throwable> GameTestHelper.expectThrows(noinline block: () -> Unit): T
{
return try {
block()
Expand All @@ -48,5 +48,3 @@ internal inline fun <reified T : Throwable> GameTestHelper.expectThrows(noinline
}
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package net.kernelpanicsoft.archie.transfer

import dev.architectury.registry.registries.RegistrySupplier
import earth.terrarium.common_storage_lib.context.ItemContext
import earth.terrarium.common_storage_lib.energy.EnergyApi
import earth.terrarium.common_storage_lib.fluid.FluidApi
import earth.terrarium.common_storage_lib.item.ItemApi
import earth.terrarium.common_storage_lib.lookup.BlockLookup
import earth.terrarium.common_storage_lib.lookup.ItemLookup
import net.minecraft.core.Direction
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityType

/**
* Exposes [ArchieItemStorage]/[ArchieFluidStorage]/[ArchieEnergyStorage] to third-party mods'
* pipes/hoppers/etc, by registering against Common Storage Lib's [ItemApi]/[FluidApi]/[EnergyApi]
* `BLOCK` lookups - which are, unlike a lookup you'd build yourself via [BlockLookup.create], the
* real, already-canonical singletons Common Storage Lib itself wires straight through to each
* platform's native capability system (Fabric Transfer API's `ItemStorage.SIDED`/`FluidStorage.SIDED`,
* NeoForge's `Capabilities.ItemHandler.BLOCK`/`Capabilities.FluidHandler.BLOCK`). Registering here
* makes a block entity's storage visible to *any* mod querying those native systems directly - no
* dependency on Common Storage Lib (or Archie) required on the consuming side.
*
* Deliberately explicit opt-in, not wired into [net.kernelpanicsoft.archie.serialization.NBTHolder.itemField]/
* `fluidField`/`energyField`: registration must happen exactly once per [BlockEntityType], while
* those field delegates run once per block entity *instance* (inside its constructor) - auto-registering
* from there would either re-register redundantly per instance or need awkward static bookkeeping.
* Call these once, at registration time, next to your `DeferredRegister`/`RegistrySupplier` declarations:
*
* ```kotlin
* object BlockEntities : ADeferredRegistryHolder<BlockEntityType<*>>(MyMod.MOD, Registries.BLOCK_ENTITY_TYPE) {
* val TANK by register("tank") { BlockEntityType.Builder.of(::TankBlockEntity, MyBlocks.TANK).build(null) }
* }
*
* // In mod init, after BlockEntities.init():
* BlockEntities.TANK.exposeFluidStorage { tank -> tank.fluid }
* ```
*/
@Suppress("unused")
object ArchieCapabilityExposure

/** See [ArchieCapabilityExposure]. Exposes this block entity type's [ArchieItemStorage] to [ItemApi.BLOCK]. */
fun <T : BlockEntity> BlockEntityType<T>.exposeItemStorage(selector: (T, Direction?) -> ArchieItemStorage?) {
exposeToBlockLookup(ItemApi.BLOCK, selector)
}

/** [exposeItemStorage] overload for a selector that doesn't need the query direction. */
fun <T : BlockEntity> BlockEntityType<T>.exposeItemStorage(selector: (T) -> ArchieItemStorage?) {
exposeItemStorage { be, _ -> selector(be) }
}

/** See [ArchieCapabilityExposure]. Exposes this block entity type's [ArchieFluidStorage] to [FluidApi.BLOCK]. */
fun <T : BlockEntity> BlockEntityType<T>.exposeFluidStorage(selector: (T, Direction?) -> ArchieFluidStorage?) {
exposeToBlockLookup(FluidApi.BLOCK, selector)
}

/** [exposeFluidStorage] overload for a selector that doesn't need the query direction. */
fun <T : BlockEntity> BlockEntityType<T>.exposeFluidStorage(selector: (T) -> ArchieFluidStorage?) {
exposeFluidStorage { be, _ -> selector(be) }
}

/** See [ArchieCapabilityExposure]. Exposes this block entity type's [ArchieEnergyStorage] to [EnergyApi.BLOCK]. */
fun <T : BlockEntity> BlockEntityType<T>.exposeEnergyStorage(selector: (T, Direction?) -> ArchieEnergyStorage?) {
exposeToBlockLookup(EnergyApi.BLOCK, selector)
}

/** [exposeEnergyStorage] overload for a selector that doesn't need the query direction. */
fun <T : BlockEntity> BlockEntityType<T>.exposeEnergyStorage(selector: (T) -> ArchieEnergyStorage?) {
exposeEnergyStorage { be, _ -> selector(be) }
}

/**
* Shared implementation: [BlockLookup] only supports registering by [BlockEntityType] via the
* [BlockLookup.BlockRegistrar] callback handed to [BlockLookup.onRegister] - `registerSelf` (the
* more direct-looking method) only accepts a `Block`-keyed getter, not a block-entity-keyed one.
*/
@Suppress("UNCHECKED_CAST")
private fun <T : BlockEntity, S> BlockEntityType<T>.exposeToBlockLookup(
lookup: BlockLookup<S, Direction?>,
selector: (T, Direction?) -> S?,
) {
lookup.onRegister { registrar ->
registrar.registerBlockEntities(
BlockLookup.BlockEntityGetter { blockEntity, direction -> selector(blockEntity as T, direction) },
this,
)
}
}

// ── RegistrySupplier convenience overloads ──────────────────────────────────────────────────
// So these can be chained right where the type is declared, without waiting for a separate
// registration-time call site. Architectury's RegistrySupplier.listen(...) already guarantees the
// callback runs once the entry is actually registered.

fun <T : BlockEntity> RegistrySupplier<BlockEntityType<T>>.exposeItemStorage(selector: (T, Direction?) -> ArchieItemStorage?) =
listen { it.exposeItemStorage(selector) }

fun <T : BlockEntity> RegistrySupplier<BlockEntityType<T>>.exposeItemStorage(selector: (T) -> ArchieItemStorage?) =
listen { it.exposeItemStorage(selector) }

fun <T : BlockEntity> RegistrySupplier<BlockEntityType<T>>.exposeFluidStorage(selector: (T, Direction?) -> ArchieFluidStorage?) =
listen { it.exposeFluidStorage(selector) }

fun <T : BlockEntity> RegistrySupplier<BlockEntityType<T>>.exposeFluidStorage(selector: (T) -> ArchieFluidStorage?) =
listen { it.exposeFluidStorage(selector) }

fun <T : BlockEntity> RegistrySupplier<BlockEntityType<T>>.exposeEnergyStorage(selector: (T, Direction?) -> ArchieEnergyStorage?) =
listen { it.exposeEnergyStorage(selector) }

fun <T : BlockEntity> RegistrySupplier<BlockEntityType<T>>.exposeEnergyStorage(selector: (T) -> ArchieEnergyStorage?) =
listen { it.exposeEnergyStorage(selector) }

// ── Item-in-item exposure (stretch) ─────────────────────────────────────────────────────────
// Unlike the BLOCK lookups above, ItemApi.ITEM has no equivalent native-platform bridge - it's
// registered under Common Storage Lib's own mod id, so this is only visible to other mods that
// also depend on Common Storage Lib and query this exact same field. Still useful: it makes a
// backpack/bag's own storage pipe-accessible (by other CSL-aware mods) even while its GUI is closed.

/** Exposes this item's [ArchieItemStorage] (e.g. a bag/backpack's contents) to [ItemApi.ITEM]. */
fun Item.exposeItemStorage(selector: (ItemStack, ItemContext) -> ArchieItemStorage?) {
ItemApi.ITEM.registerSelf(ItemLookup.ItemGetter { stack, context -> selector(stack, context) }, this)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ import kotlin.math.min
* [ArchieItemStorage]/[ArchieFluidStorage] implement - plus Archie's NBT serialization for
* save/load, mirroring their shape.
*
* Archie doesn't register this with Common Storage Lib's `EnergyApi.BLOCK`/`ITEM`/`ENTITY`
* lookups for you, the same way it doesn't for `ArchieItemStorage`/`ArchieFluidStorage` today -
* wire that lookup registration, and any platform-specific capability bridge (NeoForge's
* `IEnergyStorage`, Fabric's Team Reborn Energy API) you still want on top of it, in your own mod.
* Not exposed to third-party mods' pipes/hoppers by default - call
* [BlockEntityType.exposeEnergyStorage][net.kernelpanicsoft.archie.transfer.exposeEnergyStorage]
* once, at registration time, to register it with Common Storage Lib's `EnergyApi.BLOCK`, which
* (unlike a lookup you'd build yourself) is wired straight through to each platform's native
* capability system - no dependency on Common Storage Lib required on the consuming side. Any
* additional platform-specific bridge you still want on top of that (NeoForge's `IEnergyStorage`,
* Fabric's Team Reborn Energy API) is still on you to wire in your own mod.
*
* Usually created through [net.kernelpanicsoft.archie.serialization.NBTHolder.energyField]
* rather than directly.
Expand Down
7 changes: 4 additions & 3 deletions Archie/docs/gametest.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ class ArchieItemHandlerTests {

(`EMPTY` is `"archie:gametest/empty"`, the empty structure template most tests that don't need
actual world geometry reference.) `assertEquals`/`assertTrue`/`expectThrows` here are Archie's own
`GameTestHelper` extension helpers (`gametest/internal/tests/GameTestAssertions.kt`), not part of
vanilla — they exist purely to make failures read like a normal assertion library instead of
manually calling `fail(...)`.
public `GameTestHelper` extension helpers (`net.kernelpanicsoft.archie.gametest.GameTestAssertions.kt`,
import them like any other Archie API), not part of vanilla — they exist purely to make failures
read like a normal assertion library instead of manually calling `fail(...)`. Consuming mods can
(and should) use them too instead of hand-rolling `if (...) fail(...)` checks.

A consuming mod writes the same shape. `Archie-Test`'s own suite exercises a real block-entity-backed
menu end to end:
Expand Down
Loading