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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Written for Consumers: what a mod building against Pipeworks can use, or will se

## Unreleased

- Every Pipeworks item shows in the suite's shared **Works** creative tab (`libworks:works`) and in no vanilla tab, so the pipe, storage tank, Pump and outlet leave Functional Blocks, and the Pipeworks tab `pipeworks:items` and its lang key `itemGroup.pipeworks.items` are gone. The jar nests `libworks-runtime` 0.1.1 by jar-in-jar, so a player installs nothing more; `neoforge.mods.toml` requires `libworks` from 0.1.1 to the next minor (FactoryWorks ADR-0128, #17).
- `pipeworks:pipe` (three from three copper ingots) and `pipeworks:storage_tank` (four iron ingots, four glass and a copper ingot) have crafting recipes from vanilla items, so Pipeworks is craftable with Groundworks alone. The Pump and the creative pipe still have none (5thlayer/factoryworks#610).
- The item tag `pipeworks:closes_sides` ships `minecraft:stick`, where it shipped empty: a player with no pack can close a pipe side. A pack that names other items replaces the tag with `"replace": true`; one that adds to it keeps the stick (ADR 0004, #15).
- `pipeworks:outlet`, the **Outlet**: a port that sets its segment's fluid down as one source block in the space it faces, with no power and no recipe. It faces away from the block it is placed against and joins pipes on every other face. Once the segment holds 1,000 mB and that space is empty or holds a replaceable block (grass, snow, flowing fluid) and no source block, it takes the bucket and places the block, and the replaced block drops what it would if broken. A pipe, a torch or any other block that is only not solid is left standing. A fluid with no block stays in the pipes, and it never draws fluid from the world (#20).
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Conventional commits: `<type>(<optional scope>): <summary>`, with the summary in

## Testing

`sh ./gradlew build` runs the JUnit tests, on a plain JVM with no Minecraft. `sh ./gradlew runGameTestServer` runs the game tests headless, a real player on a real server with only Groundworks beside the Library, and names each one it ran; it fails if it ran none. JEI, EMI and Jade are on the dev client only. `PlaysAloneTest` fails on another Module's id, namespace or required dependency in the Library's code or resources, or on a tag the code reads that the game tests don't check, and `PlaysAloneTests` on a tag it reads or its data names that only another mod fills (FactoryWorks ADR-0127). A new game test class is registered by a line in `PipeworksGameTests.registerTests`, and its tests stand on the `gametest/platform` structure that the libworks plugin's `sh ./gradlew buildGametestStructures` writes. A test that needs a machine fills `gametest/TestPort`, a block registered only by the Library's own run (`pipeworks.gametest`, set in `build.gradle`), and waits a tick after placing it, since a block entity's `onLoad` runs on the next tick. CI (`.github/workflows/ci.yml`, libworks' `module-ci.yml` at the plugin's tag) runs both on every push and never publishes. Data only the Library's own runs load lives in `src/gametest/resources`, which never ships (libworks ADR 0004): `DevPacks` switches on `gametest_pack/` in the game tests and `dev_pack/` in the dev client when the Library has one, the datapacks that stand in for what a Consumer fills; it has none today. Packs a test switches on itself, such as one per batch, go under `gametest_packs/<name>/`.
`sh ./gradlew build` runs the JUnit tests, on a plain JVM with no Minecraft. `sh ./gradlew runGameTestServer` runs the game tests headless, a real player on a real server with only Groundworks and the libworks runtime the Library nests beside it, and names each one it ran; it fails if it ran none. JEI, EMI and Jade are on the dev client only. `PlaysAloneTest` fails on another Module's id, namespace or required dependency in the Library's code or resources, or on a tag the code reads that the game tests don't check, and `PlaysAloneTests` on a tag it reads or its data names that only another mod fills (FactoryWorks ADR-0127). A new game test class is registered by a line in `PipeworksGameTests.registerTests`, and its tests stand on the `gametest/platform` structure that the libworks plugin's `sh ./gradlew buildGametestStructures` writes. A test that needs a machine fills `gametest/TestPort`, a block registered only by the Library's own run (`pipeworks.gametest`, set in `build.gradle`), and waits a tick after placing it, since a block entity's `onLoad` runs on the next tick. CI (`.github/workflows/ci.yml`, libworks' `module-ci.yml` at the plugin's tag) runs both on every push and never publishes. Data only the Library's own runs load lives in `src/gametest/resources`, which never ships (libworks ADR 0004): `DevPacks` switches on `gametest_pack/` in the game tests and `dev_pack/` in the dev client when the Library has one, the datapacks that stand in for what a Consumer fills; it has none today. Packs a test switches on itself, such as one per batch, go under `gametest_packs/<name>/`.

The `skillworks:quicklaunch` skill opens the dev client into the most recent save in `run/saves`, one client per checkout.

Expand Down
4 changes: 4 additions & 0 deletions GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ _Avoid_: drain, spout, fluid placer
The pipes a Groundworks Dismantle takes up as one span: the block tag `pipeworks:dismantle/pipes`, joined only where each pipe draws an arm toward the other, so a closed side ends a span.
_Avoid_: dismantle group

**Works tab**:
The creative tab the suite's mods share, `libworks:works`, registered by the libworks runtime the Library nests. Every Pipeworks item shows there; Pipeworks registers no tab of its own (FactoryWorks ADR-0128).
_Avoid_: Pipeworks tab, creative tab (alone)

### Parties

**Pipeworks**:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Because I wanted my own fluid logistics for the -Works mods suite. Nothing speci
- **One fluid per segment.** A pipe or tank that would join two segments holding different fluids is refused, with a message saying so.
- **Breaking splits fairly.** Breaking a pipe, tank or port splits its segment by capacity, and the broken block takes its share of the fluid. Segments are saved with the world and keep their fluid while their chunks are unloaded.
- **See the level.** A storage tank shows how full its segment is, in fifteen steps, as fluid of the right colour in its glass. With [Jade](https://www.curseforge.com/minecraft/mc-mods/jade) installed, looking at a pipe, tank or port shows its segment's fluid, amount and capacity.
- **The creative pipe.** `pipeworks:creative_pipe`, in the creative tab, keeps its segment full of the fluid its screen sets. Click its slot with a bucket, or any item holding a fluid, to set it, and with an empty cursor to clear it. With JEI or EMI installed, a fluid can be dragged onto the slot.
- **The creative pipe.** `pipeworks:creative_pipe`, in the Works creative tab, keeps its segment full of the fluid its screen sets. Click its slot with a bucket, or any item holding a fluid, to set it, and with an empty cursor to clear it. With JEI or EMI installed, a fluid can be dragged onto the slot.
- **A Pump.** `pipeworks:pump` draws a still source block beside it into its segment. A source in the fluid tag `pipeworks:infinite_sources`, water by default, is left in place and gives 1,200 mB a second; any other is taken whole, as a bucket would, once the segment has room for 1,000 mB.
- **An Outlet.** `pipeworks:outlet` sets its segment's fluid down as a source block in the space it faces, a bucket's worth at a time, once that space is empty or replaceable. It never picks fluid up.
- **Dismantle and drag-laying.** Pipes are a [Groundworks](https://github.com/5thlayer/groundworks) Dismantle Family and are laid by its Stretch: sneak-click a pipe with a pickaxe, then click another to take up the run between them; drag a line of pipes the way belts are laid.
Expand Down
12 changes: 9 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,20 @@ libworks {
tagPrefix = 'v'
range = '[0.5.5,0.6)'
}
// Nested by jar-in-jar, so players install nothing more; FML loads the newest nested copy in range,
// which runs to the next minor.
runtime = '0.1.1'
}

repositories {
// Groundworks: its releases are published to the local maven repository only, and nothing else
// resolves from there.
// Groundworks and libworks-runtime: their releases are published to the local maven repository
// only, and nothing else resolves from there.
exclusiveContent {
forRepository { mavenLocal() }
filter { includeModule 'io.github.5thlayer', 'groundworks' }
filter {
includeModule 'io.github.5thlayer', 'groundworks'
includeModule 'io.github.5thlayer', 'libworks-runtime'
}
}
exclusiveContent { // EMI's unofficial 26.1.2 port, which the FactoryWorks Pack runs
forRepository { maven { url = 'https://cursemaven.com' } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package io.github._5thlayer.pipeworks;

import io.github._5thlayer.libworks.WorksTab;
import io.github._5thlayer.pipeworks.block.CreativePipeBlock;
import io.github._5thlayer.pipeworks.block.CreativePipeMenu;
import io.github._5thlayer.pipeworks.block.FluidPipeBlock;
Expand All @@ -15,12 +16,8 @@
import java.util.Set;

import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.CreativeModeTabs;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.neoforged.bus.api.IEventBus;
Expand All @@ -33,22 +30,20 @@
import net.neoforged.neoforge.registries.DeferredItem;
import net.neoforged.neoforge.registries.DeferredRegister;

/** The pipe, the storage tank, the Pump, the Outlet and the creative pipe, their items, the creative pipe's menu and the creative tab. No fluid is registered here. */
/** The pipe, the storage tank, the Pump, the Outlet and the creative pipe, their items, the creative pipe's menu, and their place in the creative tabs. No fluid is registered here. */
public final class PipeworksRegistries {

private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(Pipeworks.MOD_ID);
private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(Pipeworks.MOD_ID);
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES =
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, Pipeworks.MOD_ID);
private static final DeferredRegister<MenuType<?>> MENUS = DeferredRegister.create(Registries.MENU, Pipeworks.MOD_ID);
private static final DeferredRegister<CreativeModeTab> CREATIVE_TABS =
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, Pipeworks.MOD_ID);

public static final DeferredBlock<FluidPipeBlock> PIPE = BLOCKS.registerBlock("pipe", FluidPipeBlock::new,
props -> props.strength(1.5F, 6.0F).sound(SoundType.COPPER).requiresCorrectToolForDrops().noOcclusion());
public static final DeferredBlock<StorageTankBlock> STORAGE_TANK = BLOCKS.registerBlock("storage_tank", StorageTankBlock::new,
props -> props.strength(2.0F, 6.0F).sound(SoundType.COPPER).requiresCorrectToolForDrops().noOcclusion());
/** Has no recipe, which keeps it out of survival: it is in the creative tab and {@code /give}. */
/** Has no recipe, which keeps it out of survival: it is in the Works tab and {@code /give}. */
public static final DeferredBlock<CreativePipeBlock> CREATIVE_PIPE = BLOCKS.registerBlock("creative_pipe", CreativePipeBlock::new,
props -> props.strength(1.5F, 6.0F).sound(SoundType.COPPER).requiresCorrectToolForDrops().noOcclusion());
/** Has no recipe: a pack adds one. */
Expand All @@ -73,19 +68,6 @@ public final class PipeworksRegistries {
public static final DeferredHolder<MenuType<?>, MenuType<CreativePipeMenu>> CREATIVE_PIPE_MENU =
MENUS.register("creative_pipe", () -> IMenuTypeExtension.create(CreativePipeMenu::new));

public static final DeferredHolder<CreativeModeTab, CreativeModeTab> CREATIVE_TAB =
CREATIVE_TABS.register("items", () -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup.pipeworks.items"))
.icon(() -> new ItemStack(PIPE_ITEM.get()))
.displayItems((parameters, output) -> {
output.accept(PIPE_ITEM.get());
output.accept(STORAGE_TANK_ITEM.get());
output.accept(PUMP_ITEM.get());
output.accept(OUTLET_ITEM.get());
output.accept(CREATIVE_PIPE_ITEM.get());
})
.build());

private PipeworksRegistries() {
}

Expand All @@ -94,7 +76,6 @@ static void register(IEventBus modBus) {
ITEMS.register(modBus);
BLOCK_ENTITIES.register(modBus);
MENUS.register(modBus);
CREATIVE_TABS.register(modBus);
modBus.addListener(PipeworksRegistries::registerCapabilities);
modBus.addListener(PipeworksRegistries::addToCreativeTabs);
}
Expand All @@ -111,11 +92,12 @@ private static void registerCapabilities(RegisterCapabilitiesEvent event) {
}

private static void addToCreativeTabs(BuildCreativeModeTabContentsEvent event) {
if (event.getTabKey() == CreativeModeTabs.FUNCTIONAL_BLOCKS) {
if (event.getTabKey() == WorksTab.KEY) {
event.accept(PIPE_ITEM.get());
event.accept(STORAGE_TANK_ITEM.get());
event.accept(PUMP_ITEM.get());
event.accept(OUTLET_ITEM.get());
event.accept(CREATIVE_PIPE_ITEM.get());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2026 5thlayer
// SPDX-License-Identifier: MIT

package io.github._5thlayer.pipeworks.gametest;

import java.util.List;

import io.github._5thlayer.libworks.WorksTab;
import io.github._5thlayer.pipeworks.Pipeworks;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.CreativeModeTabs;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;

/**
* Every item the Library registers shows in the suite's shared Works tab and in no other tab, and the Library keeps no
* tab of its own (FactoryWorks ADR-0128, #17).
*/
final class CreativeTabTests {

private CreativeTabTests() {
}

static void register(PipeworksGameTests.Registrar tests) {
tests.test("the_works_tab_holds_every_item_the_library_registers", 1, CreativeTabTests::worksTabHoldsEveryItem);
tests.test("no_other_tab_holds_a_library_item", 1, CreativeTabTests::noOtherTab);
tests.test("the_library_registers_no_creative_tab", 1, CreativeTabTests::noOwnTab);
}

private static void worksTabHoldsEveryItem(GameTestHelper helper) {
CreativeModeTabs.tryRebuildTabContents(helper.getLevel().enabledFeatures(), true, helper.getLevel().registryAccess());
List<Item> shown = BuiltInRegistries.CREATIVE_MODE_TAB.getValueOrThrow(WorksTab.KEY).getDisplayItems().stream()
.map(ItemStack::getItem)
.toList();
List<Item> missing = BuiltInRegistries.ITEM.stream()
.filter(item -> BuiltInRegistries.ITEM.getKey(item).getNamespace().equals(Pipeworks.MOD_ID))
.filter(item -> !shown.contains(item))
.toList();
if (!missing.isEmpty()) {
helper.fail("the Works tab does not hold " + missing);
return;
}
helper.succeed();
}

private static void noOtherTab(GameTestHelper helper) {
CreativeModeTabs.tryRebuildTabContents(helper.getLevel().enabledFeatures(), true, helper.getLevel().registryAccess());
// Empty tabs would pass the loop below without the Library's items reaching any tab.
if (ownItems(BuiltInRegistries.CREATIVE_MODE_TAB.getValueOrThrow(WorksTab.KEY)).isEmpty()) {
helper.fail("the Works tab shows no Pipeworks item, so no tab was filled");
return;
}
for (var entry : BuiltInRegistries.CREATIVE_MODE_TAB.entrySet()) {
CreativeModeTab tab = entry.getValue();
// The search tab lists every other tab's items.
if (entry.getKey() == WorksTab.KEY || tab.getType() == CreativeModeTab.Type.SEARCH) {
continue;
}
List<Item> own = ownItems(tab);
if (!own.isEmpty()) {
helper.fail("the tab " + entry.getKey().identifier() + " shows " + own);
return;
}
}
helper.succeed();
}

private static List<Item> ownItems(CreativeModeTab tab) {
return tab.getDisplayItems().stream()
.map(ItemStack::getItem)
.filter(item -> BuiltInRegistries.ITEM.getKey(item).getNamespace().equals(Pipeworks.MOD_ID))
.toList();
}

private static void noOwnTab(GameTestHelper helper) {
var own = BuiltInRegistries.CREATIVE_MODE_TAB.keySet().stream()
.filter(id -> id.getNamespace().equals(Pipeworks.MOD_ID))
.toList();
if (!own.isEmpty()) {
helper.fail("the Library registers its own creative tab: " + own);
return;
}
helper.succeed();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import net.neoforged.fml.ModList;

/**
* The Library loads on a server with nothing else installed, at the version its build gave it. It
* is the harness's own check too: a run that reports this test ran the Library's tests, not
* vanilla's.
* The Library loads on a server with only Groundworks and the libworks runtime it nests, at the
* version its build gave it. It is the harness's own check too: a run that reports this test ran the
* Library's tests, not vanilla's.
*/
final class LoadTests {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ private static void registerTests(RegisterGameTestsEvent event) {
PumpTests.register(tests);
RecipeTests.register(tests);
OutletTests.register(tests);
CreativeTabTests.register(tests);
}

private static Identifier id(String path) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
import net.neoforged.fml.ModList;

/**
* The Library plays alone on Groundworks (FactoryWorks ADR-0127): nothing else is loaded beside it, and every tag its code
* reads or its data names holds an entry of Minecraft or Pipeworks.
* The Library plays alone on Groundworks (FactoryWorks ADR-0127): nothing else is loaded beside it but the
* libworks runtime it nests, and every tag its code reads or its data names holds an entry of Minecraft or
* Pipeworks.
*/
final class PlaysAloneTests {

private static final Set<String> LOADED_ALONE = Set.of("minecraft", "neoforge", "groundworks", Pipeworks.MOD_ID);
private static final Set<String> LOADED_ALONE = Set.of("minecraft", "neoforge", "groundworks", "libworks", Pipeworks.MOD_ID);

private static final Set<String> FILLS_ALONE = Set.of("minecraft", Pipeworks.MOD_ID);

Expand Down
9 changes: 9 additions & 0 deletions src/main/resources/META-INF/neoforge.mods.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ versionRange = "${groundworks_range}"
ordering = "NONE"
side = "BOTH"

# libworks-runtime, nested by jar-in-jar: it registers the Works creative tab. The range is the one
# libworks { runtime } in build.gradle nests, to the next minor.
[[dependencies.${mod_id}]]
modId = "libworks"
type = "required"
versionRange = "${libworks_runtime_range}"
ordering = "NONE"
side = "BOTH"

# Recipe viewers drop a fluid on the creative pipe's screen. Neither is needed to load.
[[dependencies.${mod_id}]]
modId = "jei"
Expand Down
Loading
Loading