From 64782a932b4ffa1e3e960b304677f9924a05819c Mon Sep 17 00:00:00 2001 From: Face o' Cat Date: Fri, 6 Nov 2026 13:29:17 +0200 Subject: [PATCH 1/3] Add per-face redstone controls and machine reservation overlays --- docs/gui-screens.md | 104 +++- docs/gui-sprites.md | 3 + docs/machine-companion.md | 12 +- scripts/generate_gui_sprites.py | 71 +++ .../moddev/mc/companion/CompanionMod.java | 9 - .../zone/moddev/mc/companion/DemoBlock.java | 16 + .../moddev/mc/companion/ProcessorEntity.java | 98 +++- .../assets/mmdlib_companion/lang/en_us.json | 1 + .../resources/mmdlib/screen/crusher.xml | 9 +- .../resources/mmdlib/screen/dual_crusher.xml | 4 - .../resources/mmdlib/screen/transmuter.xml | 4 - .../companion/test/AutomationClientSmoke.java | 3 +- .../test/AutomationRecoveryTests.java | 4 +- .../companion/test/CompanionClientSmoke.java | 55 +- .../mc/companion/test/CompanionGameTests.java | 430 +++++++++----- .../companion/test/RedstoneClientSmoke.java | 236 ++++++++ .../mc/companion/test/RedstoneSmoke.java | 413 ++++++++++++++ .../test/ReservationClientSmoke.java | 176 ++++++ .../mc/lib/api/redstone/IRedstoneOutput.java | 14 + .../mc/lib/api/redstone/RedstoneControl.java | 268 +++++++++ .../mc/lib/api/redstone/RedstoneOutput.java | 57 ++ .../mc/lib/gui/client/AutomationDrawer.java | 92 +-- .../mc/lib/gui/client/DrawerMemory.java | 28 +- .../mc/lib/gui/client/FaceConfigRenderer.java | 100 ++++ .../mc/lib/gui/client/MachineScreen.java | 55 +- .../mc/lib/gui/client/RedstoneDrawer.java | 535 ++++++++++++++++++ .../mc/lib/gui/client/ReservationOverlay.java | 68 +++ .../mc/lib/gui/layout/RedstoneLayout.java | 41 ++ .../mc/lib/gui/runtime/AutomationMenu.java | 6 + .../mc/lib/gui/runtime/MachineMenu.java | 22 +- .../mc/lib/gui/runtime/RedstoneMenu.java | 209 +++++++ .../mc/lib/gui/runtime/RedstoneNetwork.java | 129 +++++ .../moddev/mc/lib/gui/runtime/ScreenHost.java | 5 + .../mc/lib/gui/runtime/ScreenNetwork.java | 3 +- .../mc/lib/machine/feature/FeatureHost.java | 27 +- .../lib/machine/feature/MachineFeature.java | 5 + .../lib/machine/feature/RecipeProcessor.java | 84 ++- .../resources/assets/mmdlib/lang/en_us.json | 44 +- .../gui/sprites/icon_processor_progress.png | Bin 0 -> 127 bytes .../gui/sprites/icon_redstone_between.png | Bin 0 -> 196 bytes .../gui/sprites/icon_redstone_outside.png | Bin 0 -> 192 bytes .../gui/sprites/icon_redstone_revert.png | Bin 0 -> 123 bytes .../gui/sprites/icon_redstone_revert_on.png | Bin 0 -> 123 bytes .../moddev/mc/lib/gui/ScreenLoaderTest.java | 10 +- 44 files changed, 3109 insertions(+), 341 deletions(-) create mode 100644 src/companionTest/java/zone/moddev/mc/companion/test/RedstoneClientSmoke.java create mode 100644 src/companionTest/java/zone/moddev/mc/companion/test/RedstoneSmoke.java create mode 100644 src/companionTest/java/zone/moddev/mc/companion/test/ReservationClientSmoke.java create mode 100644 src/main/java/zone/moddev/mc/lib/api/redstone/IRedstoneOutput.java create mode 100644 src/main/java/zone/moddev/mc/lib/api/redstone/RedstoneControl.java create mode 100644 src/main/java/zone/moddev/mc/lib/api/redstone/RedstoneOutput.java create mode 100644 src/main/java/zone/moddev/mc/lib/gui/client/FaceConfigRenderer.java create mode 100644 src/main/java/zone/moddev/mc/lib/gui/client/RedstoneDrawer.java create mode 100644 src/main/java/zone/moddev/mc/lib/gui/client/ReservationOverlay.java create mode 100644 src/main/java/zone/moddev/mc/lib/gui/layout/RedstoneLayout.java create mode 100644 src/main/java/zone/moddev/mc/lib/gui/runtime/RedstoneMenu.java create mode 100644 src/main/java/zone/moddev/mc/lib/gui/runtime/RedstoneNetwork.java create mode 100644 src/main/resources/assets/mmdlib/textures/gui/sprites/icon_processor_progress.png create mode 100644 src/main/resources/assets/mmdlib/textures/gui/sprites/icon_redstone_between.png create mode 100644 src/main/resources/assets/mmdlib/textures/gui/sprites/icon_redstone_outside.png create mode 100644 src/main/resources/assets/mmdlib/textures/gui/sprites/icon_redstone_revert.png create mode 100644 src/main/resources/assets/mmdlib/textures/gui/sprites/icon_redstone_revert_on.png diff --git a/docs/gui-screens.md b/docs/gui-screens.md index 0fad73f1..e1e129ac 100644 --- a/docs/gui-screens.md +++ b/docs/gui-screens.md @@ -66,18 +66,31 @@ contents, and asks the client block entity's `screenItems()` what each slot acce holds, so a click is predicted the way the server will answer it. It's only asked about rules, so its contents don't need to be synced. -Return the `ItemStorage` itself. A wrapper around it, or another `ReservedSlots` implementation, -loses the guards below and the client's copy of the plans. +Return the `ItemStorage` itself, or a handler/wrapper implementing `ReservedSlots`, including +`plan(slot)`, so both menu sides receive and enforce the same reservations. When the machine has reserved items in a slot (see [capabilities](capabilities.md#reservations)), the menu keeps them from being taken or replaced: - Left and right click, Q and Ctrl+Q, and shift-click take only what is free. - Clicking the slot with a different item is refused. -- A number key is refused on a slot with a plan, because vanilla would move the whole stack. +- Number-key and offhand swaps move only the unreserved portion. - Double-clicking a slot with a plan doesn't collect from elsewhere. Double-clicking another slot still takes the planned slot's free part. +Machine slots show reservations at the top right. Both reservation and held quantities use +87.5% of vanilla's font size: +`-2` means two items reserved for input, and `+1` means space reserved for one output. The held +quantity stays at the bottom right, using the same size even when no reservation is present. +An empty planned output shows its item model at 50% +opacity with `+1` at the top, without creating a real stack. Mixed consume/produce plans show +both signed counts on the top row. Tooltips provide reservation counts and the planned output's +name. Clearing the plan removes the badge and ghost. Player inventory, equipment and filter +slots are unaffected. +Chance outputs show their reserved capacity, not a guarantee that the product will be rolled. +No extra packet or XML binding is needed; `MachineMenu.reservationPlan(index)` exposes the existing +synchronized snapshot for real machine slots. + The client's menu works out the same answer from the slot's plan, which arrives with the screen's values, so clicks don't flicker. Bars and numbers still come from the getters named in the XML; a feature's own client sync (see [block entities](block-entities.md#machine-features)) doesn't @@ -338,7 +351,7 @@ payload contains both lane index and state. The server restricts that selector t in its XML property. These settings persist with the machine. The graphical smoke run checks focus, typing, cancellation, resizing, view changes, release outside bounds and server rejection. -The expanded menu protocol is version **6**, including a session token in menu-opening data. +The expanded menu protocol is version **9**, including a session token in menu-opening data. Client and server must use matching MMDLib builds. ## Files and IDs @@ -446,7 +459,7 @@ inventory slots keeps the machine's `screenDestinations` routing. JEI recipe tra only the ordinary inventory and hotbar, leaving equipped items alone. Optional `` goes after the main body and before tabs/popouts. Its button -children form a right-aligned row above the player inventory (for example redstone and pause +children form a right-aligned row above the player inventory (for example manual pause toggles). Automation's reusable `InventoryColorButtonBar` shares that row on the left, with scrollable overflow. Both sides align with the nine-column storage grid, leaving the equipment column clear. Flat controls fit in the inventory's existing top row; taller controls add only @@ -817,6 +830,87 @@ to the available slot groups or counts closes the old view and invalidates stale reopening shows the updated slots. Curios stays optional, with no tab or class-loading requirement when absent. This drawer does not change slot definitions or rendering toggles. +## Redstone face drawer + +Implement `ScreenHost.screenRedstone()` to return a persistent `RedstoneControl`. Its right-side +drawer uses the inventory drawer's neighbour grid, directional badges and shared frame. Clicking +a different face selects it; clicking the selected face cycles Ignore, Read and Emit (right-click +cycles backwards). Faces have a spaced selection outline and small background-free signal numbers: +bright green incoming strength for Read, orange emitted strength for Emit, with thin glyph outlines. + +- **Ignore:** no input gate and no emitted signal. +- **Read:** a single bar has a minimum handle above and a maximum below. Endpoints are inclusive, + and dragging either past the other pushes both together. Between highlights the selected interval; + Outside highlights its complement. The icon toggle sits to the left of the bar: inward arrows + between boundaries mean Between, outward arrows beyond boundaries mean Outside. Equal endpoints + simplify only the info text to For N / Except N; ranges touching 0 or 15 use a single + above/below condition (including equality for Between). The icon stays unchanged. Processing pauses when + the face receives a highlighted level. + The default is Between 1–15. Dragging previews locally, release applies both endpoints atomically, + and Escape cancels the drag. Any Read face requesting pause holds processing. +- **Emit:** click the colour swatch or sprite and source name to cycle outputs; right-click + cycles backwards. Resource fullness entries come first in the companion, followed by each + processor's own Processing, Paused/idle and Progress entries. A small reverse-arrows button + before the picker toggles Revert (`15 - level`). Until a source is selected, or if that source + no longer exists, the face emits zero even when reverted. + +These controls are built in: there are no redstone upgrade items or slots. Each face saves its +interval, Between/Outside choice, stable output ID and inversion independently. Changing the +face mode retains those settings. The small yellow info box explains the selected source and +current signal. The drawer fits its content and scrolls when the viewport cannot fit it. The +old main redstone button is removed; manual pause remains above the inventory. Right drawers +are mutually exclusive, and drawer/face selection is remembered for the current connection. + +### Output providers + +Runtime parts opt in by implementing `api.redstone.IRedstoneOutput`. Its `redstoneOutputs()` +returns a stable, ordered list of `RedstoneOutput` descriptors. Each descriptor has: + +- A stable ID unique across this machine, used for saved selections. +- Translatable name and description components, used by the picker and yellow info box. +- An RGB colour and optional GUI sprite ID, used as the picker's swatch or icon. +- A server-side `IntSupplier` whose result is clamped to 0-15. + +Construct `RedstoneControl(owner, parts)` after registering the parts; it discovers only parts +implementing the interface and rejects duplicate IDs. The catalog must have the same order and +metadata on client and server. Descriptors use common-side types, without client rendering +classes. Suppliers are evaluated only on the server, once per selected source per output update. +`FeatureHost` implements the interface by collecting outputs from participating features. + +`RedstoneOutput.resource(group, fullnessSupplier)` adapts an existing `ResourceGroup`, preserving +its ID, label and colour. Fullness is 0-1 and maps to `floor(fullness * 15)`. The companion measures +item groups by average slot fullness, fluid groups by amount/capacity, and energy by stored/capacity. +The resource-only `RedstoneControl(owner, groups, fullnessFunction)` constructor remains available. + +Each `RecipeProcessor` contributes three independent sources, prefixed by its saved feature key: + +| Source | Signal | +|---|---| +| Processing | 15 while actively processing (including the start and completion ticks), otherwise 0 | +| Paused/idle | 15 while paused, idle, blocked, energy-stalled or held by the host; otherwise 0 | +| Progress | `floor(completedTicks / recipeTicks * 15)`; retained while held/paused, 15 on completion, 0 when idle | + +Use the constructor accepting a display-name `Component` to label processors clearly; the original +constructor defaults to the feature key. Multiple processors produce multiple sets of entries. +There is no aggregate machine-wide running signal. + +Tick in this order on the server: + +```java +redstone.readInputs(); +features.tick(level, !redstone.paused() && otherProcessingGates); +// Perform any automatic transfers here. +redstone.updateOutputs(); +``` + +Passing `false` to `FeatureHost.tick` invokes each feature's `suspended` callback instead of +processing it, so a held processor cannot retain a stale running signal. Save/load the controller +tag and return `signal(direction)` from the block's redstone output hook. The direction passed +to `signal` is Minecraft's query direction, not the outward face. Stateless hosts can call +`redstone.tick()` to read inputs and update outputs together. The companion emits weak power, +keeps transfers running while processing is held, and migrates old inventory selections and +the previous global redstone mode. Manual pause remains independent. + ## Proper use These are guidance for screen authors, not rules MMDLib can check: diff --git a/docs/gui-sprites.md b/docs/gui-sprites.md index 4c86103d..3e172a71 100644 --- a/docs/gui-sprites.md +++ b/docs/gui-sprites.md @@ -31,7 +31,10 @@ icons and progress arrows are full colour. | `icon_pause`, `icon_resume` | 16×16 | none | Gold pause bars and green play triangle; `_a` and `_b` variants identify independent lanes | | `button_flat`, `button_flat_highlighted` | 14×14 | nine-slice, border 1 | Compact button with a uniform dark outline and flat grey fill; hover lightens only the interior | | `icon_filter_whitelist`, `icon_filter_blacklist` | 10×10 | none | White/black list cards on the filter's flat grey mode toggle | +| `icon_processor_progress` | 16x16 | none | Three rising blue bars for per-processor recipe progress. | | `icon_redstone_ignore`, `icon_redstone_high`, `icon_redstone_low` | 16×16 | none | Redstone torch with × / + / − mode badge | +| `icon_redstone_between`, `icon_redstone_outside` | 32x12 | none | Transparent interval glyphs with cream boundaries and green inward/outward arrows. | +| `icon_redstone_revert`, `icon_redstone_revert_on` | 14x14 | none | Opposed horizontal arrows for output inversion; cream when off, green when on. | | `icon_info` | 16×16 | none | Pop-out `info`: white serif "i" with a dark outline and transparent background | | `icon_warning` | 16×16 | none | Pop-out `warning`: yellow triangle, black "!" | | `icon_error` | 16×16 | none | Pop-out `error`: red heater shield, white "!" | diff --git a/docs/machine-companion.md b/docs/machine-companion.md index 0c2e3b4d..732a431b 100644 --- a/docs/machine-companion.md +++ b/docs/machine-companion.md @@ -19,7 +19,7 @@ to run without it. | Block ID (prefix `mmdlib_companion:`) | What it does | | --- | --- | -| `crusher` | One input slot, one output slot, one processor, and a water/lava preview tank | +| `crusher` | One input slot, two output slots, one processor, and a water/lava preview tank | | `transmuter` | One slot limited to one item; converts an iron ingot into a gold ingot | | `dual_crusher` | Two independently pausable processors share the same input and output slots | | `creative_power` | Refills its buffer and supplies up to 1,000 FE per adjacent face per tick | @@ -29,6 +29,14 @@ sand, and copper/iron/gold ore becomes two of the corresponding raw material. Th 200 ticks and 4,000 FE. The dual crusher runs the same recipes in both lanes when there are enough inputs and output space. +The Crusher's two outputs share one inventory and colour/side configuration. For testing, both +allow insertion and extraction, so you can manually fill them with any item; reservations still +protect promised output space. New Crushers default this group to input/output on all faces. Outputs may +split across the slots: with 63 raw iron in each output, one iron ore reserves `+1` in each slot +and finishes at 64 + 64. With only one free space total, that two-item recipe waits without taking +input or energy. Existing Crusher saves retain their input and first output, with the new output +starting empty. The Dual Crusher keeps its existing shared single-output layout. + The bottom of each processor panel has a compact, right-aligned row of icon buttons above the inventory, with thin dark borders and flat grey backgrounds. Click the pause icon to interrupt a lane and release its reservations; it becomes a green play @@ -43,7 +51,7 @@ inside to read longer text. All four screens place a vertical battery on the rig reusable `` XML widget. Its red charge bars fill upwards inside a slim casing. The energy and fluid gauges share matching edges and medium-grey empty backgrounds, darker than slots. Energy bars are thin with a soft reflection; cylindrical glass shading covers only the fluid. -The Crusher centers its input, a circular progress dial and output alongside it. The dial is a +The Crusher centers its input, a circular progress dial and two adjacent output slots. The dial is a custom widget registered by the companion through MMDLib's [widget API](gui-screens.md#registering-custom-widget-types). Click the dial to pause or resume; its tooltip shows progress in ticks. The Crusher's Recipes button opens crushing recipes when JEI is installed. The other processors retain their progress diff --git a/scripts/generate_gui_sprites.py b/scripts/generate_gui_sprites.py index d049b7f5..55e930b7 100644 --- a/scripts/generate_gui_sprites.py +++ b/scripts/generate_gui_sprites.py @@ -298,6 +298,74 @@ def glyph(img, rows, x, y, ink): write_png("icon_redstone_" + mode, img) +def write_processor_progress_icon(): + """Three rising blue bars inside a small progress frame.""" + img = canvas(16, 16) + rect(img, 1, 2, 14, 12, (30, 40, 52, 255)) + rect(img, 2, 3, 12, 10, (92, 108, 124, 255)) + for x, height in ((3, 3), (7, 6), (11, 9)): + rect(img, x, 12 - height, 2, height, (133, 210, 255, 255)) + rect(img, x, 12 - height, 2, 1, (213, 241, 255, 255)) + write_png("icon_processor_progress", img) + + +def write_redstone_revert_icons(): + """Opposed horizontal arrows for normal/reverted resource output, transparent 14x14.""" + rows = ("......#...", "......##..", "#########.", "......##..", "......#...") + ink = (30, 40, 34, 255) + for active in (False, True): + img = canvas(14, 14) + for y0, right in ((1, True), (8, False)): + for y, row in enumerate(rows): + for x, bit in enumerate(row if right else row[::-1]): + if bit == "#": + img[y0 + y][x + 2] = ((173, 255, 192, 255) if active + else (255, 239, 183, 255)) + result = canvas(14, 14) + for y, row in enumerate(img): + for x, colour in enumerate(row): + if colour == CLEAR: continue + for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)): + nx, ny = x + dx, y + dy + if 0 <= nx < 14 and 0 <= ny < 14: result[ny][nx] = ink + for y, row in enumerate(img): + for x, colour in enumerate(row): + if colour != CLEAR: result[y][x] = colour + write_png("icon_redstone_revert" + ("_on" if active else ""), result) + + +def write_redstone_range_icons(): + """32x12 transparent interval glyphs, matching the outlined machine-control sprites.""" + ink = (30, 40, 34, 255) + arrow_rows = ("...#...", "...##..", "######.", "#######", + "######.", "...##..", "...#...") + for mode in ("between", "outside"): + img = canvas(32, 12) + bars = (2, 28) if mode == "between" else (12, 18) + arrows = ((7, True), (18, False)) if mode == "between" else ((2, False), (23, True)) + for x in bars: + rect(img, x, 2, 2, 8, (210, 186, 121, 255)) + rect(img, x, 2, 1, 7, (255, 239, 183, 255)) + for x, right in arrows: + for y, row in enumerate(arrow_rows): + for dx, bit in enumerate(row if right else row[::-1]): + if bit == "#": + img[y + 2][x + dx] = ((173, 255, 192, 255) if y < 3 + else (86, 218, 127, 255) if y < 5 + else (53, 165, 96, 255)) + result = canvas(32, 12) + for y, row in enumerate(img): + for x, colour in enumerate(row): + if colour == CLEAR: continue + for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)): + nx, ny = x + dx, y + dy + if 0 <= nx < 32 and 0 <= ny < 12: result[ny][nx] = ink + for y, row in enumerate(img): + for x, colour in enumerate(row): + if colour != CLEAR: result[y][x] = colour + write_png("icon_redstone_" + mode, result) + + def write_side_config_sprites(): """Outlined directional glyphs remain legible over faded neighbour previews.""" cyan, orange = (133, 210, 255, 255), (255, 185, 101, 255) @@ -361,6 +429,9 @@ def main(): write_png("compact_frame", compact_frame(), {"type": "nine_slice", "width": 8, "height": 8, "border": 1}) write_drawer_sprites() write_control_icons() + write_redstone_range_icons() + write_redstone_revert_icons() + write_processor_progress_icon() write_filter_icons() write_flat_buttons() write_side_config_sprites() diff --git a/src/companion/java/zone/moddev/mc/companion/CompanionMod.java b/src/companion/java/zone/moddev/mc/companion/CompanionMod.java index f1e5e16a..b2194f6b 100644 --- a/src/companion/java/zone/moddev/mc/companion/CompanionMod.java +++ b/src/companion/java/zone/moddev/mc/companion/CompanionMod.java @@ -93,15 +93,6 @@ public CompanionMod(IEventBus bus) { id("tank_container"), MaterialEventTypes.GUI_CLICK, CompanionMod::transferTankContainer); - MaterialExtensions.GLOBAL - .events() - .registerCallback( - id("redstone"), - MaterialEventTypes.GUI_CLICK, - click -> { - if (click.menu().blockEntity instanceof ProcessorEntity machine) - machine.cycleRedstoneMode(); - }); for (int lane = 0; lane < 2; lane++) { int index = lane; MaterialExtensions.GLOBAL diff --git a/src/companion/java/zone/moddev/mc/companion/DemoBlock.java b/src/companion/java/zone/moddev/mc/companion/DemoBlock.java index ad12296a..fbf2c8fa 100644 --- a/src/companion/java/zone/moddev/mc/companion/DemoBlock.java +++ b/src/companion/java/zone/moddev/mc/companion/DemoBlock.java @@ -32,6 +32,22 @@ public DemoBlock(Properties properties, Supplier> ty this.type = type; } + @Override + protected boolean isSignalSource(BlockState state) { + return true; + } + + @Override + protected int getSignal( + BlockState state, + net.minecraft.world.level.BlockGetter level, + BlockPos pos, + net.minecraft.core.Direction side) { + return level.getBlockEntity(pos) instanceof ProcessorEntity entity + ? entity.redstone.signal(side) + : 0; + } + @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return type.get().create(pos, state); diff --git a/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java b/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java index eb41a738..9ee43cfb 100644 --- a/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java +++ b/src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java @@ -23,6 +23,8 @@ import net.neoforged.neoforge.fluids.FluidStack; import zone.moddev.mc.lib.api.automation.*; +import zone.moddev.mc.lib.api.redstone.RedstoneOutput; +import zone.moddev.mc.lib.api.redstone.RedstoneControl; import zone.moddev.mc.lib.api.capabilities.*; import zone.moddev.mc.lib.api.capabilities.FluidStorage; import zone.moddev.mc.lib.api.capabilities.ItemStorage; @@ -53,8 +55,7 @@ public enum Kind { public final FeatureHost features = new FeatureHost(this::setChanged, () -> FeatureSync.send(this)); private final Kind kind; - // 0 ignores redstone, 1 requires a signal, 2 requires no signal. Manual pause is separate. - private int redstoneMode; + public final RedstoneControl redstone; private String machineName = "Crusher"; private int reservePercent; @@ -91,6 +92,7 @@ public ProcessorEntity(BlockEntityType type, BlockPos pos, BlockState state, fluid -> fluid.getFluid() == Fluids.WATER || fluid.getFluid() == Fluids.LAVA); if (kind == Kind.LOGISTICS) for (int i = 0; i < 4; i++) fluids.addTank(4000, i < 2 ? Role.INPUT : Role.OUTPUT); + int[] outputSlots = kind == Kind.CRUSHER ? new int[] {1, 2} : new int[] {1}; var inventory = MachineInventory.builder().group("energy", StorageGroups.energy(energy)); if (kind == Kind.TRANSMUTER) { // One key, and one item: the recipe consumes the whole slot before inserting gold. @@ -98,24 +100,35 @@ public ProcessorEntity(BlockEntityType type, BlockPos pos, BlockState state, inventory.group("slot", StorageGroups.items(items, 0)); } else { items = - new ItemStorage(2).role(Role.INPUT, 0).role(Role.OUTPUT, 1).onChange(this::setChanged); + new ItemStorage(1 + outputSlots.length) + .role(Role.INPUT, 0) + .role(kind == Kind.CRUSHER ? Role.STORAGE : Role.OUTPUT, outputSlots) + .onChange(this::setChanged); inventory .group("input", StorageGroups.items(items, 0)) - .group("output", StorageGroups.items(items, 1)); + .group("output", StorageGroups.items(items, outputSlots)); } var shared = inventory.build(); var recipeType = kind == Kind.TRANSMUTER ? CompanionMod.TRANSMUTING : CompanionMod.CRUSHING; var processors = new ArrayList(); for (int i = 0; i < (kind == Kind.DUAL ? 2 : 1); i++) { - var processor = new RecipeProcessor("lane_" + i, recipeType, shared); + var processor = + new RecipeProcessor( + "lane_" + i, + recipeType, + shared, + Component.translatable("screen.mmdlib_companion.processor", i + 1)); processors.add(processor); features.add(processor); } lanes = List.copyOf(processors); var groups = new ArrayList>(); groups.add( - itemGroup("input", 0, 0x568bd8, kind == Kind.TRANSMUTER ? IoMode.BOTH : IoMode.INPUT)); - if (kind != Kind.TRANSMUTER) groups.add(itemGroup("output", 1, 0xe48f45, IoMode.OUTPUT)); + itemGroup("input", 0x568bd8, kind == Kind.TRANSMUTER ? IoMode.BOTH : IoMode.INPUT, 0)); + if (kind != Kind.TRANSMUTER) + groups.add( + itemGroup( + "output", 0xe48f45, kind == Kind.CRUSHER ? IoMode.BOTH : IoMode.OUTPUT, outputSlots)); int[] colours = {0x64bfc1, 0xa678cb, 0xe0c55d, 0xce668e}; for (int i = 0; i < fluids.getTanks(); i++) { final int tank = i; @@ -149,10 +162,39 @@ public ProcessorEntity(BlockEntityType type, BlockPos pos, BlockState state, energyGroup.bind(() -> Direction.NORTH, side -> energy.view(() -> energyGroup.mode(side))); groups.add(energyGroup); automation = new AutomationInventory(this, groups); + var outputParts = new ArrayList(); + for (var group : groups) + outputParts.add( + RedstoneOutput.resource( + group, + () -> { + if (group.type() == ResourceType.ENERGY) + return energy.capacity() == 0 ? 0 : (double) energy.stored() / energy.capacity(); + double amount = 0, capacity = 0; + for (int index : group.members()) { + if (group.type() == ResourceType.ITEMS && index < items.getSlots()) { + var stack = items.getStackInSlot(index); + int limit = + Math.min( + items.getSlotLimit(index), + stack.isEmpty() ? 64 : stack.getMaxStackSize()); + if (limit > 0) { + amount += (double) stack.getCount() / limit; + capacity++; + } + } else if (group.type() == ResourceType.FLUIDS && index < fluids.getTanks()) { + amount += fluids.getFluidInTank(index).getAmount(); + capacity += fluids.getTankCapacity(index); + } + } + return capacity == 0 ? 0 : amount / capacity; + })); + if (kind != Kind.LOGISTICS) outputParts.add(features); + redstone = new RedstoneControl(this, outputParts); } private ResourceGroup itemGroup( - String id, int slot, int colour, IoMode mode) { + String id, int colour, IoMode mode, int... slots) { var group = new ResourceGroup<>( id, @@ -160,25 +202,35 @@ private ResourceGroup itemGroup( colour, ResourceType.ITEMS, mode, - List.of(slot), - List.of(id), + java.util.Arrays.stream(slots).boxed().toList(), + java.util.stream.IntStream.range(0, slots.length) + .mapToObj(i -> i == 0 ? id : id + "_" + (i + 1)) + .toList(), new SideConfig(mode)); return group.bind( () -> Direction.NORTH, - side -> items.view(index -> index == slot ? group.mode(side) : IoMode.DISABLED)); + side -> items.view(index -> group.contains(index) ? group.mode(side) : IoMode.DISABLED)); } @Override + public RedstoneControl screenRedstone() { + return redstone; + } + public AutomationInventory screenAutomation() { return automation; } @Override public void serverTick() { - if (kind != Kind.LOGISTICS - && redstoneAllowsRun() - && energy.stored() >= energy.capacity() * reservePercent / 100) features.tick(level); + redstone.readInputs(); + features.tick( + level, + kind != Kind.LOGISTICS + && redstoneAllowsRun() + && energy.stored() >= energy.capacity() * reservePercent / 100); AutomationTransfers.tick(automation, energy); + redstone.updateOutputs(); } public FluidStack getTank1() { @@ -193,17 +245,8 @@ public FluidStack getTank3() { return fluids.getFluidInTank(3); } - public int getRedstoneMode() { - return redstoneMode; - } - - public void cycleRedstoneMode() { - redstoneMode = (redstoneMode + 1) % 3; - setChanged(); - } - private boolean redstoneAllowsRun() { - return redstoneMode == 0 || (level.hasNeighborSignal(worldPosition) == (redstoneMode == 1)); + return !redstone.paused(); } public void toggle(int index) { @@ -316,7 +359,7 @@ protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) super.saveAdditional(tag, registries); tag.put("items", items.serializeNBT(registries)); tag.put("automation", automation.save(registries)); - tag.putInt("redstone_mode", redstoneMode); + tag.put("redstone_control", redstone.save(registries)); tag.putString("machine_name", machineName); tag.putInt("reserve_percent", reservePercent); if (hasTank()) tag.put("fluids", fluids.serializeNBT(registries)); @@ -328,7 +371,10 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) if (FeatureSync.read(features, tag, registries)) return; super.loadAdditional(tag, registries); int savedMode = tag.getInt("redstone_mode"); - redstoneMode = savedMode >= 0 && savedMode <= 2 ? savedMode : 0; + int redstoneMode = savedMode >= 0 && savedMode <= 2 ? savedMode : 0; + if (tag.contains("redstone_control")) + redstone.load(tag.getCompound("redstone_control"), registries); + else redstone.migrate(redstoneMode); String savedName = tag.getString("machine_name"); machineName = !savedName.isBlank() diff --git a/src/companion/resources/assets/mmdlib_companion/lang/en_us.json b/src/companion/resources/assets/mmdlib_companion/lang/en_us.json index 13f3285a..82157d80 100644 --- a/src/companion/resources/assets/mmdlib_companion/lang/en_us.json +++ b/src/companion/resources/assets/mmdlib_companion/lang/en_us.json @@ -1,4 +1,5 @@ { + "screen.mmdlib_companion.processor": "Processor %s", "itemGroup.mmdlib_companion": "MMDLib Machines", "screen.mmdlib_companion.energy": "Energy", "jei.mmdlib_companion.crushing": "Crushing", diff --git a/src/companion/resources/mmdlib/screen/crusher.xml b/src/companion/resources/mmdlib/screen/crusher.xml index 624cf681..22748392 100644 --- a/src/companion/resources/mmdlib/screen/crusher.xml +++ b/src/companion/resources/mmdlib/screen/crusher.xml @@ -16,7 +16,10 @@ tooltip-fallback="Progress: {value} / {max} ticks. Click to pause or resume."> - + + + + @@ -44,10 +47,6 @@ -