diff --git a/CHANGELOG.md b/CHANGELOG.md index b2efce2a..ac47b116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Before a release, `./gradlew releaseChangelog` turns *Unreleased* into the relea ## [Unreleased] +### Added + +- NEEPMeat compatibility (Fabric 1.20.1): vascular conduits power turret bases (1 eJ = 1 E); a full turret base takes no power from the network + ## [4.0.0] - 2026-11-03 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 501ef9dd..4a8aa06f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,8 @@ Core gameplay: Jar-in-jar it (approved). - **Jade** (optional integration, `compat/jade/` for the server half and `client/compat/jade/` for the tooltips, registered through the `jade` entrypoint on Fabric and `@WailaPlugin` on NeoForge and Forge). Compile-only (`modCompileOnly`, Modrinth maven), never bundled and never required at runtime. +- **NEEPMeat** (Fabric 1.20.1 only, Modrinth `maven.modrinth:neepmeat`, version in `versions/1.20.1-fabric/gradle.properties`; requested by the user on 2026-11-04): its vascular network charges the turret bases through OMLib (`platform/fabric/FabricNeepMeatPower`, registered by `FabricBlockLookups` when NEEPMeat is loaded; 1 eJ = 1 E, a full buffer takes nothing), so the mod has no code of its own for it. + Never bundled (`suggests` in `fabric.mod.json`); `modCompileOnly` for the game test `gametest/platform/fabric/NeepMeatGameTests` (in `src/gametest/mc1.20.1-1.20.1`; the other nodes drop its line from the test mod's `fabric-gametest` entrypoints), and `modLocalRuntime` with the mods nested in its jar (Loom leaves them out of its remapped copy; OMLib's `nestedModJars`) and **GeckoLib**, which it requires, for the dev runs and the game test server. - **NeoForge** 21.1.x (1.21.1) and 21.11.x (1.21.11), built with **ModDevGradle** (approved by the user; build-time only). On NeoForge, the Fabric-only APIs above are replaced by NeoForge's own (capabilities, events, payload registration); see ยง4 "Loaders". - **Forge** 47.x (1.20.1 only), built with the legacy variant of ModDevGradle (`net.neoforged.moddev.legacyforge`, same artifact and version as `moddev`; build-time only), requested by the user. diff --git a/README.md b/README.md index 88c9a7ca..cd9c0523 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Open Modular Turrets has long been a popular choice in PvP modpacks, and it is n - **Generator:** burns furnace fuel (9 slots) and powers adjacent turret bases and other machines; an upgrade slot multiplies production and fuel use together - **Turret API for other mods:** add turrets with their own weapons, ammo, models and custom hit logic; see [docs/turret-api.md](docs/turret-api.md) - Uses **Team Reborn Energy** on Fabric, so it works with energy from other Fabric tech mods; the NeoForge build uses its energy capability instead +- Optional **NEEPMeat** support (Fabric 1.20.1): vascular conduits power turret bases, 1 eJ per energy unit - Optional **Jade** support for turret bases and turrets, on both loaders ## Requirements diff --git a/build.fabric.gradle.kts b/build.fabric.gradle.kts index b70f063c..6f186bc1 100644 --- a/build.fabric.gradle.kts +++ b/build.fabric.gradle.kts @@ -1,3 +1,5 @@ +import java.util.concurrent.Callable + // Fabric nodes (Loom). Shared logic (Java, metadata templating, tests, publishing) is in buildSrc/mod-common. plugins { id("mod-common") @@ -96,15 +98,48 @@ dependencies { "modClientRuntimeOnly"("maven.modrinth:sable:${property("sable_version")}") } + // NEEPMeat (1.20.1 only; the other nodes have no neepmeat_version): OMLib's FabricNeepMeatPower lets its vascular + // network charge the bases. Not bundled, the mod needs nothing of it; NEEPMeat (with the mods nested in its jar, + // which Loom leaves out of its remapped copy: nestedModJars in omlib/build-logic) and GeckoLib (which it requires, + // also with the library nested in its jar) are in the dev runs and on the game test server, where + // gametest/platform/fabric/NeepMeatGameTests checks the bases + findProperty("neepmeat_version")?.let { version -> + val neepmeat = "maven.modrinth:neepmeat:$version" + modCompileOnly(neepmeat) + modLocalRuntime(neepmeat) + // resolved lazily, when Loom resolves modLocalRuntime (earlier, it would use the repository before Loom sets it up) + val geckolib = "maven.modrinth:geckolib:${property("geckolib_version")}" + modLocalRuntime(geckolib) + // GeckoLib nests a library too (mclib; without it the client's resource reload fails) + val nested = layout.buildDirectory.dir("nested-mods") + modLocalRuntime(files(Callable { + listOf(neepmeat, geckolib).flatMap { notation -> + val jar = configurations.detachedConfiguration(dependencies.create(notation)).apply { isTransitive = false }.singleFile + nestedModJars(jar, nested.get().asFile) + } + })) + } + // Architecture rules (src/test/.../architecture): they read the compiled main and client classes testImplementation("com.tngtech.archunit:archunit-junit5:${property("archunit_version")}") testRuntimeOnly(sourceSets["client"].output) } -// The game test source set also gets the client's runtime dependencies; Sable (see above) must stay off the test -// server. Loom renames remapped mods to the group remapped.; Jade is compile-only, so nothing else is affected. -configurations.named("gametestRuntimeClasspath") { - exclude(group = "remapped.maven.modrinth") +// The game test source set also gets the client's runtime dependencies; Sable (see above, 1.21.1) must stay off the +// test server. Loom renames remapped mods to the group remapped. (and their modules to -); Jade is +// compile-only, so nothing else is affected there. 1.20.1 has no Sable: NEEPMeat and GeckoLib stay on its test server. +if (findProperty("sable_companion_version") != null) { + configurations.named("gametestRuntimeClasspath") { + exclude(group = "remapped.maven.modrinth") + } +} + +// NeepMeatGameTests exists only where NEEPMeat does (src/gametest/mc1.20.1-1.20.1): the other nodes drop its line from +// the test mod's fabric-gametest entrypoints +if (findProperty("neepmeat_version") == null) { + tasks.named("processGametestResources") { + filesMatching("fabric.mod.json") { filter { line -> if ("NeepMeatGameTests" in line) null else line } } + } } // ---- Test coverage (JaCoCo): check prints the line coverage of core and api ---- diff --git a/docs/architecture.md b/docs/architecture.md index 850816c9..77d1833b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -51,6 +51,7 @@ Two directions, so that gameplay code stays loader-free: Each entrypoint installs its implementations first thing; the `compat` facades (`NetworkCompat`, `MenuCompat`, `ClientNetworkCompat`) forward to them. Energy and items other blocks may use are described loader-free (`core.energy.EnergyAccess`, `inventory.ItemAccess`, exposed by `getEnergyAccess(side)` / `getItemAccess(side)`) and wrapped by thin adapters per loader (Team Reborn Energy and Transfer API on Fabric, FE and item handler capabilities on NeoForge and Forge). +On Fabric 1.20.1 with NEEPMeat installed, OMLib also makes them sinks of its vascular network (`FabricNeepMeatPower`, 1 eJ = 1 E). ### How core talks to the game diff --git a/omlib b/omlib index f3afdaf4..552f6b67 160000 --- a/omlib +++ b/omlib @@ -1 +1 @@ -Subproject commit f3afdaf453f2ec5eeb1e9c7cf48bc63bd7dbe885 +Subproject commit 552f6b67c864739e281bc05cc6f17f314da17cdf diff --git a/src/gametest/mc1.20.1-1.20.1/java/omtteam/openmodularturrets/gametest/platform/fabric/NeepMeatGameTests.java b/src/gametest/mc1.20.1-1.20.1/java/omtteam/openmodularturrets/gametest/platform/fabric/NeepMeatGameTests.java new file mode 100644 index 00000000..f5c80bd3 --- /dev/null +++ b/src/gametest/mc1.20.1-1.20.1/java/omtteam/openmodularturrets/gametest/platform/fabric/NeepMeatGameTests.java @@ -0,0 +1,107 @@ +package omtteam.openmodularturrets.gametest.platform.fabric; + +import com.neep.neepmeat.transport.api.pipe.BloodAcceptor; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import omtteam.omlib.core.energy.EnergyStore; +import omtteam.openmodularturrets.blockentity.TurretBaseBlockEntity; +import omtteam.openmodularturrets.gametest.OMTGameTests; +import omtteam.openmodularturrets.registry.ModBlocks; + +/** + * NEEPMeat (Fabric 1.20.1 only): its vascular network charges turret bases at 1 eJ = 1 E (OMLib's + * {@code FabricNeepMeatPower}), a base taking only what its energy still accepts. Only this node has NEEPMeat on the + * game test server; the other Fabric nodes leave the class out of the test mod's entrypoints. + */ +@SuppressWarnings("unused") +public class NeepMeatGameTests implements OMTGameTests { + private static final BlockPos EMITTER = new BlockPos(1, 1, 1); + private static final BlockPos CONDUIT = EMITTER.east(); + private static final BlockPos BASE = CONDUIT.east(); + /** Power of the emitter in the network test, eJ per tick (below the tier three base's 1000 E/t intake). */ + private static final int EMITTER_POWER = 400; + /** Tier one base: its energy accepts 50 E per tick and holds 5000 (config defaults). */ + private static final long TIER_ONE_INTAKE = 50; + + /** + * Power emitter, vascular conduit, tier three base: the conduit finds the base as a sink when placed, and the base + * is charged with the emitter's whole output, in whole ticks of it. + */ + @GameTest(template = EMPTY_STRUCTURE, batch = "neepmeat_network", timeoutTicks = 200) + public void vascularNetworkChargesABase(GameTestHelper helper) { + helper.setBlock(EMITTER, block("power_emitter")); + // its saved power (the block entity's class extends one of a library nested in NEEPMeat's jar, not on the + // compile classpath) + CompoundTag power = new CompoundTag(); + power.putInt("power", EMITTER_POWER); + helper.getBlockEntity(EMITTER).load(power); + helper.setBlock(BASE, ModBlocks.TURRET_BASES.get(2)); + EnergyStore energy = ((TurretBaseBlockEntity) helper.getBlockEntity(BASE)).getLogic().getEnergy(); + placeAsPlayer(helper, CONDUIT, block("vascular_conduit")); + helper.succeedWhen(() -> { + long amount = energy.getAmount(); + helper.assertTrue(amount >= EMITTER_POWER * 20L, "The base holds only " + amount + " E"); + helper.assertTrue(amount % EMITTER_POWER == 0, + "The base holds " + amount + " E, not whole ticks of " + EMITTER_POWER + " eJ"); + clear(helper); + }); + } + + /** + * The base as a sink: a plain (not active) sink that claims only its energy's intake per tick, charges that much + * each tick, asks the network for a new share when it is full and then claims nothing. + */ + @GameTest(template = EMPTY_STRUCTURE, batch = "neepmeat_sink", timeoutTicks = 300) + public void baseClaimsOnlyWhatItsEnergyAccepts(GameTestHelper helper) { + helper.setBlock(BASE, ModBlocks.TURRET_BASES.get(0)); + EnergyStore energy = ((TurretBaseBlockEntity) helper.getBlockEntity(BASE)).getLogic().getEnergy(); + BloodAcceptor sink = BloodAcceptor.SIDED.find(helper.getLevel(), helper.absolutePos(BASE), Direction.WEST); + helper.assertTrue(sink != null, "The base is no sink of a vascular network"); + helper.assertTrue(sink.getMode() == BloodAcceptor.Mode.SINK, "The base is a " + sink.getMode() + " sink"); + int[] updates = {0}; + sink.setChangeListener(changed -> updates[0]++); + helper.assertTrue(sink.getDemandHint() == TIER_ONE_INTAKE, "The base asks for " + sink.getDemandHint() + " eJ/t"); + // 1 per unit = 1000 eJ/t on offer: the base takes its 50 + float taken = sink.updateInflux(1f); + helper.assertTrue(Math.round(taken * 1000) == TIER_ONE_INTAKE, "The base took " + taken * 1000 + " eJ/t of 1000"); + long start = helper.getTick(); + helper.succeedWhen(() -> { + helper.assertTrue(energy.getFreeSpace() == 0, "The base is not full yet (" + energy.getAmount() + " E)"); + long ticks = helper.getTick() - start; + long expected = energy.getCapacity() / TIER_ONE_INTAKE; + helper.assertTrue(Math.abs(ticks - expected) <= 2, "Filling took " + ticks + " ticks, not " + expected); + helper.assertTrue(updates[0] > 0, "The full base did not ask the network for a new share"); + helper.assertTrue(sink.getDemandHint() == 0, "The full base still asks for " + sink.getDemandHint() + " eJ/t"); + helper.assertTrue(sink.updateInflux(1f) == 0f, "The full base still takes power"); + sink.setChangeListener(null); + clear(helper); + }); + } + + private static Block block(String id) { + return BuiltInRegistries.BLOCK.get(new ResourceLocation("neepmeat", id)); + } + + /** Places a block like a player does: its state from its neighbours, then its placement callback. */ + private static void placeAsPlayer(GameTestHelper helper, BlockPos pos, Block block) { + BlockPos absolute = helper.absolutePos(pos); + BlockState state = Block.updateFromNeighbourShapes(block.defaultBlockState(), helper.getLevel(), absolute); + helper.getLevel().setBlockAndUpdate(absolute, state); + block.setPlacedBy(helper.getLevel(), absolute, state, null, ItemStack.EMPTY); + } + + private static void clear(GameTestHelper helper) { + helper.setBlock(CONDUIT, Blocks.AIR); + helper.setBlock(EMITTER, Blocks.AIR); + helper.setBlock(BASE, Blocks.AIR); + } +} diff --git a/src/gametest/resources/fabric.mod.json b/src/gametest/resources/fabric.mod.json index 1fdb393f..83795c0a 100644 --- a/src/gametest/resources/fabric.mod.json +++ b/src/gametest/resources/fabric.mod.json @@ -16,6 +16,7 @@ "omtteam.openmodularturrets.gametest.GeneratorGameTests", "omtteam.openmodularturrets.gametest.BaseGuiGameTests", "omtteam.openmodularturrets.gametest.ItemTransferGameTests", + "omtteam.openmodularturrets.gametest.platform.fabric.NeepMeatGameTests", "omtteam.openmodularturrets.gametest.CamoGameTests", "omtteam.openmodularturrets.gametest.CamoMenuGameTests", "omtteam.openmodularturrets.gametest.BlockMoveGameTests", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index f455b451..e17d8e43 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -39,7 +39,8 @@ } ], "suggests": { - "jade": "*" + "jade": "*", + "neepmeat": "*" }, "depends": { "fabricloader": ">=${loader_version}", diff --git a/versions/1.20.1-fabric/gradle.properties b/versions/1.20.1-fabric/gradle.properties index 5595035d..71358f38 100644 --- a/versions/1.20.1-fabric/gradle.properties +++ b/versions/1.20.1-fabric/gradle.properties @@ -3,3 +3,7 @@ minecraft_dependency=1.20.1 fabric_api_version=0.92.12+1.20.1 java_version=17 jade_version=11.13.3+fabric +# NEEPMeat (Fabric, 1.20.1 only; Modrinth): its vascular network charges the bases (OMLib); in the dev runs and on the +# game test server with GeckoLib, which it requires (4.8.4 Fabric) +neepmeat_version=0.34.0-beta+1.20.1 +geckolib_version=SD2lOScS