diff --git a/AGENTS.md b/AGENTS.md index 30567bc5e..7da1aa989 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 { block }` – assert `block` throws `ExceptionType`, returns the caught exception diff --git a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt b/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt index 7a9d3b7ca..ea5e65a74 100644 --- a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt +++ b/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt @@ -9,6 +9,9 @@ internal fun AEvents.ArchieGameTestBuilder.archieTestGameTests() client { register() } + server { + register() + } } internal object ArchieTestGameTest : AGameTestEventObject(ArchieTest.MOD) diff --git a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTestFixtures.kt b/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTestFixtures.kt new file mode 100644 index 000000000..351c8f368 --- /dev/null +++ b/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTestFixtures.kt @@ -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 + } + } +} diff --git a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTests.kt b/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTests.kt new file mode 100644 index 000000000..3975bb113 --- /dev/null +++ b/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTests.kt @@ -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() + } +} diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt b/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt index 527a31c88..029315c1b 100644 --- a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt +++ b/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt @@ -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 @@ -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 diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt index 55699df45..8bf0ca92a 100644 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt +++ b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt @@ -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 diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt index 7a9400ac9..20252e262 100644 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt +++ b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt @@ -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 diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt index 4b2b640ca..42ede1ede 100644 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt +++ b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt @@ -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 diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/GameTestAssertions.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt similarity index 78% rename from Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/GameTestAssertions.kt rename to Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt index 675ce93e5..be5a0238a 100644 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/GameTestAssertions.kt +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt @@ -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()) @@ -17,7 +17,7 @@ internal fun GameTestHelper.assertTrue(condition: Boolean, message: () -> String * * @param message Failure message builder; defaults to reporting both values. */ -internal fun GameTestHelper.assertEquals( +fun GameTestHelper.assertEquals( expected: T, actual: T, message: () -> String = { "Expected <$expected>, got <$actual>" }, @@ -34,7 +34,7 @@ internal fun GameTestHelper.assertEquals( * * @return The caught exception of type [T]. */ -internal inline fun GameTestHelper.expectThrows(noinline block: () -> Unit): T +inline fun GameTestHelper.expectThrows(noinline block: () -> Unit): T { return try { block() @@ -48,5 +48,3 @@ internal inline fun GameTestHelper.expectThrows(noinline } } } - - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt new file mode 100644 index 000000000..be325218f --- /dev/null +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt @@ -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>(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 BlockEntityType.exposeItemStorage(selector: (T, Direction?) -> ArchieItemStorage?) { + exposeToBlockLookup(ItemApi.BLOCK, selector) +} + +/** [exposeItemStorage] overload for a selector that doesn't need the query direction. */ +fun BlockEntityType.exposeItemStorage(selector: (T) -> ArchieItemStorage?) { + exposeItemStorage { be, _ -> selector(be) } +} + +/** See [ArchieCapabilityExposure]. Exposes this block entity type's [ArchieFluidStorage] to [FluidApi.BLOCK]. */ +fun BlockEntityType.exposeFluidStorage(selector: (T, Direction?) -> ArchieFluidStorage?) { + exposeToBlockLookup(FluidApi.BLOCK, selector) +} + +/** [exposeFluidStorage] overload for a selector that doesn't need the query direction. */ +fun BlockEntityType.exposeFluidStorage(selector: (T) -> ArchieFluidStorage?) { + exposeFluidStorage { be, _ -> selector(be) } +} + +/** See [ArchieCapabilityExposure]. Exposes this block entity type's [ArchieEnergyStorage] to [EnergyApi.BLOCK]. */ +fun BlockEntityType.exposeEnergyStorage(selector: (T, Direction?) -> ArchieEnergyStorage?) { + exposeToBlockLookup(EnergyApi.BLOCK, selector) +} + +/** [exposeEnergyStorage] overload for a selector that doesn't need the query direction. */ +fun BlockEntityType.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 BlockEntityType.exposeToBlockLookup( + lookup: BlockLookup, + 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 RegistrySupplier>.exposeItemStorage(selector: (T, Direction?) -> ArchieItemStorage?) = + listen { it.exposeItemStorage(selector) } + +fun RegistrySupplier>.exposeItemStorage(selector: (T) -> ArchieItemStorage?) = + listen { it.exposeItemStorage(selector) } + +fun RegistrySupplier>.exposeFluidStorage(selector: (T, Direction?) -> ArchieFluidStorage?) = + listen { it.exposeFluidStorage(selector) } + +fun RegistrySupplier>.exposeFluidStorage(selector: (T) -> ArchieFluidStorage?) = + listen { it.exposeFluidStorage(selector) } + +fun RegistrySupplier>.exposeEnergyStorage(selector: (T, Direction?) -> ArchieEnergyStorage?) = + listen { it.exposeEnergyStorage(selector) } + +fun RegistrySupplier>.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) +} 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 2f5e0fd94..c7139fd71 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 @@ -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. diff --git a/Archie/docs/gametest.md b/Archie/docs/gametest.md index 3ecebfa02..5dbdebdb8 100644 --- a/Archie/docs/gametest.md +++ b/Archie/docs/gametest.md @@ -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: