Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 electric lights (1 eJ = 1 E); a full electric light takes no power from the network

## [1.0.0] - 2026-11-03

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Core gameplay:
Jade creates every plugin it finds on dedicated servers too, where the client class cannot be loaded (Fabric refuses it, NeoForge 1.21.11 fails on its client types), so `OMLJadeClientPlugin` is not registered with Jade: `OMLJadePlugin.registerClient`, which Jade calls only on a client, creates it by name and forwards to it (fixed on 2026-10-27: before, dedicated servers with Jade logged errors, and on 1.21.11 they crashed).
The NeoForge and Forge `client` source sets get Jade through `clientCompileOnly` extending `compileOnly`.
Jade is not on the NeoForge GameTest server: there it sends a packet to the tests' vanilla mock players, which NeoForge refuses.
- **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 electric lights 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/NeepMeatGameTest` (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.
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ On a server, the server's settings are sent to the players.
With [Jade](https://modrinth.com/mod/jade) installed, looking at a light shows its mode and whether it is inverted.
Jade is not required.

### Optional: NEEPMeat

On Fabric 1.20.1, [NEEPMeat](https://modrinth.com/mod/neepmeat)'s vascular conduits power the electric lights, 1 eJ per energy unit.
A full light takes no power from the network.

## Requirements

- Fabric: Minecraft 1.20.1 (Java 17), 1.21.1 or 1.21.11 (Java 21), with [Fabric Loader](https://fabricmc.net/) and [Fabric API](https://modrinth.com/mod/fabric-api)
Expand Down
32 changes: 32 additions & 0 deletions build.fabric.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -67,6 +69,28 @@ dependencies {
modCompileOnly("maven.modrinth:jade:${property("jade_version")}")
modLocalRuntime("maven.modrinth:jade:${property("jade_version")}")

// NEEPMeat (1.20.1 only; the other nodes have no neepmeat_version): OMLib's FabricNeepMeatPower lets its vascular
// network charge the electric lights. 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/NeepMeatGameTest checks a floodlight
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)
Expand All @@ -75,6 +99,14 @@ dependencies {
testImplementation("nl.jqno.equalsverifier:equalsverifier:${property("equalsverifier_version")}")
}

// NeepMeatGameTest 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<ProcessResources>("processGametestResources") {
filesMatching("fabric.mod.json") { filter { line -> if ("NeepMeatGameTest" in line) null else line } }
}
}

// ---- Test coverage (JaCoCo): check prints the line coverage of core ----
// Both the unit tests and the game test server record coverage. Active version only (all Fabric nodes share the source).
// Report: versions/<v>/build/reports/jacoco/test/html/index.html
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ Outside access goes through the loader:
The adapters are all OMLib's:

- Energy: `core.energy.EnergyAccess`, exposed as Team Reborn Energy on Fabric (`FabricEnergyStorages`) and as an FE capability on NeoForge (`NeoForgeEnergyHandler`; on 1.21.11 an `EnergyHandler` of NeoForge's transfer API, in OMLib's version folder `src/main/mc1.21.11-`) and Forge (`ForgeEnergyHandler`).
On Fabric 1.20.1 with NEEPMeat installed, OMLib also makes the lights sinks of its vascular network (`FabricNeepMeatPower`, 1 eJ = 1 E).
Only inserting is possible; there is no extraction.
- Items: `inventory.ItemAccess.insertOnly`, exposed as `ItemStorage.SIDED` on Fabric (`FabricItemStorages`) and as `IItemHandler` on NeoForge and Forge (NeoForge 1.21.11: `ResourceHandler<ItemResource>`).
The NeoForge capabilities are registered by `NeoForgeCapabilities`, one class per version folder of OMLib.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package omtteam.openmodularlighting.gametest.platform.fabric;

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.openmodularlighting.block.FloodlightBlock;
import omtteam.openmodularlighting.blockentity.ElectricFloodlightBlockEntity;
import omtteam.openmodularlighting.gametest.OMLGameTests;
import omtteam.openmodularlighting.registry.ModBlocks;

/**
* NEEPMeat (Fabric 1.20.1 only): its vascular network charges an electric floodlight at 1 eJ = 1 E (OMLib's
* {@code FabricNeepMeatPower}; the sink's rules are tested in OpenModularTurrets). 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 NeepMeatGameTest implements OMLGameTests {
private static final BlockPos EMITTER = new BlockPos(1, 1, 3);
private static final BlockPos CONDUIT = EMITTER.east();
private static final BlockPos LIGHT = CONDUIT.east();
/** Power of the emitter, eJ per tick. */
private static final int EMITTER_POWER = 1000;

/**
* Power emitter, vascular conduit, electric floodlight (off, so it uses nothing): the conduit finds the light as a
* sink when placed, and the light is charged with the emitter's whole output, in whole ticks of it.
*/
@GameTest(template = EMPTY_STRUCTURE, timeoutTicks = 200)
public void vascularNetworkChargesAFloodlight(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(LIGHT, ModBlocks.ELECTRIC_FLOODLIGHT.defaultBlockState().setValue(FloodlightBlock.FACING, Direction.UP));
EnergyStore energy = ((ElectricFloodlightBlockEntity) helper.getBlockEntity(LIGHT)).getEnergyStore();
placeAsPlayer(helper, CONDUIT, block("vascular_conduit"));
helper.succeedWhen(() -> {
long amount = energy.getAmount();
helper.assertTrue(amount >= EMITTER_POWER * 20L, "The floodlight holds only " + amount + " E");
helper.assertTrue(amount % EMITTER_POWER == 0,
"The floodlight holds " + amount + " E, not whole ticks of " + EMITTER_POWER + " eJ");
helper.setBlock(CONDUIT, Blocks.AIR);
helper.setBlock(EMITTER, Blocks.AIR);
helper.setBlock(LIGHT, Blocks.AIR);
});
}

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);
}
}
1 change: 1 addition & 0 deletions src/gametest/resources/fabric.mod.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"omtteam.openmodularlighting.gametest.UvFloodlightGameTest",
"omtteam.openmodularlighting.gametest.GrowLightGameTest",
"omtteam.openmodularlighting.gametest.JadeTooltipGameTest",
"omtteam.openmodularlighting.gametest.platform.fabric.NeepMeatGameTest",
"omtteam.openmodularlighting.gametest.CamoGameTest"
]
},
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/fabric.mod.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"omlib": ">=${omlib_version}"
},
"suggests": {
"jade": "*"
"jade": "*",
"neepmeat": "*"
}
}
4 changes: 4 additions & 0 deletions versions/1.20.1-fabric/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ fabric_api_version=0.92.12+1.20.1
java_version=17
team_reborn_energy_version=3.0.0
jade_version=11.13.3+fabric
# NEEPMeat (Fabric, 1.20.1 only; Modrinth): its vascular network charges the electric lights (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