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
76 changes: 76 additions & 0 deletions docs/gui-screens.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,82 @@ under `build/gui-smoke*`, checks server actions and full codecs, opens the actua
synced values, and saves `screenshots/gui-smoke.png`. Run a normal build afterward to remove
injected smoke classes from main output; release verification rejects any leaked smoke classes.

For a side-configuration-only drawer, return `false` from
`ScreenHost.screenAutomationAddons()`. The menu registers no addon/filter slots and the drawer
ends below its face controls. Combine this with `inventory="none"` to omit player inventory and
equipment drawers, as in the companion Creative Power Source.

## Statistics drawer

The left Statistics drawer shows sections supplied by machine parts through
`api.stats.StatsProvider`. The menu knows only this interface, not concrete storage or processor
classes. Return the contributing parts from `ScreenHost.screenStats()` in display order:

```java
@Override public List<StatsProvider> screenStats() {
var parts = new ArrayList<StatsProvider>();
parts.add(energy);
parts.add(items);
parts.add(fluids);
parts.addAll(lanes);
return List.copyOf(parts);
}
```

Return stable part instances so the menu can retain counter history. Repeated references to the
same part are sampled once. Providers run only on the server, once per 20 game ticks per menu;
unit changes are local and use the existing synchronized values channel.

A provider returns `Statistics`: a translated section title, rows with stable IDs, and optional
energy-demand contributions. Rows describe a cumulative `Counter`, an exact `Rate`, explanatory
`Text`, or aggregate `MaximumUse`. Each row declares its label, unit, precision and whether it
scales with time. `row.whenUsed(resource)` hides it unless a provider reports positive
energy demand for that exact resource. The drawer handles sampling and formatting. Custom parts can implement this
interface without changing the menu or renderer. Each section supports at most 64 distinct rows;
the combined wire snapshot is limited to 4,096 lines. Empty sections add no title or rows.

Built-in providers:

- `ItemStorage` and `FluidStorage` return `Statistics.EMPTY`.
- `RecipeProcessor` uses its display name as header, followed by measured cycles and maximum
cycles per selected timescale. The maximum is based on the active recipe's duration; without an
active plan it says No recipe. A manually interrupted processor has released its plan, so its
maximum is unavailable until it reserves again. The just-finished plan bridges the one-tick
gap before the next recipe. Idle/reset processors do not retain stale recipe limits.
- `EnergyBuffer` shows RF in and its maximum transfer rate when it supports input, and RF out and
its maximum transfer rate when it supports output. RF used and RF max use appear only while
registered processors report positive current-recipe demand for this buffer. Generators and idle
buffers omit both usage rows. Beneath the Energy header there are two rows per supported
direction, plus two usage rows when applicable.

Input/output maxima follow the selected timescale: the configured transfer limit is the RF/t
baseline, multiplied by 20 for RF/s or 1,200 for RF/min. This uses one transfer per tick;
the capability still enforces the limit per call, so multiple callers can exceed this nominal
rate. Measured input/output continues to report actual total flow.
Maximum processing use is the sum of each registered processor's active recipe peak
`ceil(recipe energy / recipe duration)` for this buffer, scaled to the selected time unit.
`EnergyGroup.statisticsResource()` identifies the backing storage; `StorageGroups.energy` returns
the exact buffer instance. This attributes demand correctly when lanes share buffers or a machine
has multiple independent buffers. Register all contributing processors, not just the buffer.

`EnergyBuffer.insert` counts incoming energy, `extract` counts outgoing energy, and `consume`
counts processing use separately. Simulations, direct assignments, loading and resizing do not
count. Recipe processors call `EnergyGroup.consume`; its default delegates to `extract` for custom
implementations, while the built-in adapter records use separately. Transactions restore all
three counters along with stored energy on rollback. Creative Power refills through `insert`.
Counters are transient; saved NBT remains unchanged.

Measured RF uses the most recent 20 ticks. Completed cycles use up to 1,200 ticks (60 game seconds),
warming up from menu opening. Stalls, interruptions and unsuccessful output commits do not count
as completions. Rates use game time at 20 ticks per second, not wall-clock time. A first sample
shows a dash until enough time has elapsed. RF values are grouped integers; cycles have exactly
two decimal places. For example: 24,000 RF/min and 12.00 cycles/min.

The compact header selector cycles Per tick / Per second / Per minute; right-click reverses.
Info, Statistics and equipment drawers replace each other on the left. Right-side automation
and redstone drawers remain independent. Statistics scrolls when needed, reserves its bounds
from JEI, closes on Escape, and remembers its open state for the connection.

## Registering custom widget types

A consuming mod can add a leaf widget without replacing MMDLib's screen or menu. Register its
Expand Down
16 changes: 14 additions & 2 deletions docs/machine-companion.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,27 @@ apply to `runClient` and `runCompanionClient` only and stay outside the reposito
Minecraft uses its default window size. Automated smoke runs retain their own fixed dimensions.

Place a **Creative Power Source** next to a machine, then right-click the machine to open its screen.
The source supplies all six adjacent faces. JEI is included in development runs; add `-PwithoutJei`

Open the Creative Power Source and use its right-hand side configuration tab to configure each
output face. Its screen has only side configuration, Info and Statistics panels, with no player
inventory, equipment drawers or addon/filter slots.
Machines face the player when placed. Left/right in the face grid are viewed from that front,
so neighbor icons and side controls follow the same orientation. Rotation and mirroring update
that facing. Blocks placed before facing was recorded keep the north default; replace them to
choose their front from placement. Walking around a machine does not change its configured front.

Faces cycle between Output and Disabled. Disabled faces block both automatic delivery and
external extraction; settings survive reloads. Existing sources start with all six faces enabled.

JEI is included in development runs; add `-PwithoutJei`
to run without it.

| Block ID (prefix `mmdlib_companion:`) | What it does |
| --- | --- |
| `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 |
| `creative_power` | Refills its buffer and supplies up to 1,000 FE per enabled adjacent face per tick |

The crusher recipes take 100 ticks and 2,000 FE per cycle: cobblestone becomes gravel, gravel becomes
sand, and copper/iron/gold ore becomes two of the corresponding raw material. The converter takes
Expand Down
9 changes: 7 additions & 2 deletions src/companion/java/zone/moddev/mc/companion/CompanionMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

import zone.moddev.mc.lib.api.MaterialEventTypes;
import zone.moddev.mc.lib.api.MaterialExtensions;
import zone.moddev.mc.lib.api.capabilities.IoMode;
import zone.moddev.mc.lib.api.capabilities.MachineCapabilities;
import zone.moddev.mc.lib.gui.runtime.ScreenClick;
import zone.moddev.mc.lib.machine.neoforge.MachineRecipeTypes;
Expand Down Expand Up @@ -138,7 +137,13 @@ private static void capabilities(RegisterCapabilitiesEvent event) {
for (var machine : MACHINES) {
if (machine == CREATIVE_POWER) {
MachineCapabilities.energy(
event, machine.type.get(), entity -> entity.energy, (entity, side) -> IoMode.OUTPUT);
event,
machine.type.get(),
entity -> entity.energy,
(entity, side) ->
((CreativePowerEntity) entity)
.automation.mode(
zone.moddev.mc.lib.api.automation.ResourceType.ENERGY, 0, side));
continue;
}
event.registerBlockEntity(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,58 @@

import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.Clearable;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;

import net.neoforged.neoforge.capabilities.Capabilities;

import zone.moddev.mc.lib.api.automation.AutomationInventory;
import zone.moddev.mc.lib.api.automation.ResourceGroup;
import zone.moddev.mc.lib.api.automation.ResourceType;
import zone.moddev.mc.lib.api.capabilities.IoMode;
import zone.moddev.mc.lib.api.capabilities.ItemStorage;
import zone.moddev.mc.lib.api.capabilities.SideConfig;
import zone.moddev.mc.lib.common.runtime.DropsContents;
import zone.moddev.mc.lib.gui.runtime.ScreenHost;

/** Refills each tick and supplies up to 1,000 FE per adjacent face per tick. */
public final class CreativePowerEntity extends DemoEntity implements ScreenHost {
public final class CreativePowerEntity extends DemoEntity
implements ScreenHost, DropsContents, Clearable {
private final ItemStorage empty = new ItemStorage(0);
public final AutomationInventory automation;

public CreativePowerEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
energy.setRates(0, 1_000);
energy.setStored(energy.capacity());
var group =
new ResourceGroup<>(
"energy",
Component.translatable("screen.mmdlib_companion.group.energy"),
0xd85b5b,
ResourceType.ENERGY,
IoMode.OUTPUT,
java.util.List.of(0),
java.util.List.of("energy"),
new SideConfig(IoMode.OUTPUT));
group.bind(this::facing, side -> energy.view(() -> group.mode(side)));
automation = new AutomationInventory(this, java.util.List.of(group));
}

@Override
public void serverTick() {
energy.setStored(energy.capacity());
energy.insert(energy.space(), false);
for (var side : Direction.values()) {
if (!automation.mode(ResourceType.ENERGY, 0, side).canExtract()) continue;
var target = worldPosition.relative(side);
if (!level.hasChunkAt(target)) continue;
var receiver = level.getCapability(Capabilities.EnergyStorage.BLOCK, target, side.getOpposite());
var receiver =
level.getCapability(Capabilities.EnergyStorage.BLOCK, target, side.getOpposite());
if (receiver != null && receiver.canReceive())
energy.extract(receiver.receiveEnergy(1_000, false), false);
}
Expand All @@ -36,4 +63,45 @@ public void serverTick() {
public ItemStorage screenItems() {
return empty;
}

@Override
public AutomationInventory screenAutomation() {
return automation;
}

@Override
public boolean screenAutomationAddons() {
return false;
}

@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
tag.put("automation", automation.save(registries));
}

@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
if (tag.contains("automation")) automation.load(tag.getCompound("automation"), registries);
}

@Override
public void dropContents(Level level, BlockPos pos) {
automation.dropAll(
payload ->
net.minecraft.world.Containers.dropItemStack(
level,
pos.getX() + .5,
pos.getY() + .5,
pos.getZ() + .5,
RecoveryCapsuleItem.create(payload)));
}

@Override
public void clearContent() {
automation.clear();
energy.setStored(0);
setChanged();
}
}
29 changes: 29 additions & 0 deletions src/companion/java/zone/moddev/mc/companion/DemoBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,26 @@
import java.util.function.Supplier;

import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.ItemInteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.Mirror;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.phys.BlockHitResult;

import net.neoforged.neoforge.fluids.FluidUtil;
Expand All @@ -25,11 +32,33 @@

/** Explicit block hooks for a code-registered machine. */
public final class DemoBlock extends Block implements EntityBlock {
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
private final Supplier<BlockEntityType<DemoEntity>> type;

public DemoBlock(Properties properties, Supplier<BlockEntityType<DemoEntity>> type) {
super(properties);
this.type = type;
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
}

@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING);
}

@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
}

@Override
public BlockState rotate(BlockState state, Rotation rotation) {
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
}

@Override
public BlockState mirror(BlockState state, Mirror mirror) {
return rotate(state, mirror.getRotation(state.getValue(FACING)));
}

@Override
Expand Down
8 changes: 8 additions & 0 deletions src/companion/java/zone/moddev/mc/companion/DemoEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ protected DemoEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {

public abstract void serverTick();

public net.minecraft.core.Direction facing() {
return zone.moddev.mc.lib.api.capabilities.RelativeFace.facing(getBlockState());
}

public java.util.List<? extends zone.moddev.mc.lib.api.stats.StatsProvider> screenStats() {
return java.util.List.of(energy);
}

public long getEnergy() {
return energy.stored();
}
Expand Down
23 changes: 16 additions & 7 deletions src/companion/java/zone/moddev/mc/companion/ProcessorEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.util.List;

import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
Expand All @@ -23,12 +22,12 @@
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;
import zone.moddev.mc.lib.api.capabilities.Role;
import zone.moddev.mc.lib.api.redstone.RedstoneControl;
import zone.moddev.mc.lib.api.redstone.RedstoneOutput;
import zone.moddev.mc.lib.common.runtime.DropsContents;
import zone.moddev.mc.lib.gui.runtime.ScreenHost;
import zone.moddev.mc.lib.machine.feature.FeatureHost;
Expand Down Expand Up @@ -144,7 +143,7 @@ public ProcessorEntity(BlockEntityType<?> type, BlockPos pos, BlockState state,
new SideConfig(
kind == Kind.CRUSHER ? IoMode.BOTH : (i < 2 ? IoMode.INPUT : IoMode.OUTPUT)));
group.bind(
() -> Direction.NORTH,
this::facing,
side -> fluids.view(index -> index == tank ? group.mode(side) : IoMode.DISABLED));
groups.add(group);
}
Expand All @@ -159,7 +158,7 @@ public ProcessorEntity(BlockEntityType<?> type, BlockPos pos, BlockState state,
List.of("energy"),
new SideConfig(kind == Kind.LOGISTICS ? IoMode.BOTH : IoMode.INPUT));
if (kind == Kind.LOGISTICS) energy.setRates(1000, 1000);
energyGroup.bind(() -> Direction.NORTH, side -> energy.view(() -> energyGroup.mode(side)));
energyGroup.bind(this::facing, side -> energy.view(() -> energyGroup.mode(side)));
groups.add(energyGroup);
automation = new AutomationInventory(this, groups);
var outputParts = new ArrayList<Object>();
Expand Down Expand Up @@ -208,7 +207,7 @@ private ResourceGroup<net.neoforged.neoforge.items.IItemHandler> itemGroup(
.toList(),
new SideConfig(mode));
return group.bind(
() -> Direction.NORTH,
this::facing,
side -> items.view(index -> group.contains(index) ? group.mode(side) : IoMode.DISABLED));
}

Expand Down Expand Up @@ -263,6 +262,14 @@ public int getProgress() {
return lanes.getFirst().progress();
}

@Override
public List<zone.moddev.mc.lib.api.stats.StatsProvider> screenStats() {
var parts = new ArrayList<zone.moddev.mc.lib.api.stats.StatsProvider>();
parts.add(energy);
parts.addAll(lanes);
return List.copyOf(parts);
}

public boolean hasTank() {
return fluids.getTanks() > 0;
}
Expand Down Expand Up @@ -304,7 +311,9 @@ public List<Component> getWarnings() {
warnings.add(Component.translatable("screen.mmdlib_companion.redstone.waiting"));
for (int i = 0; i < lanes.size(); i++) {
var status = lanes.get(i).status();
if (status == RecipeProcessor.Status.STALLED || status == RecipeProcessor.Status.BLOCKED)
if (status == RecipeProcessor.Status.STALLED
|| status == RecipeProcessor.Status.BLOCKED
|| status == RecipeProcessor.Status.HELD)
warnings.add(
Component.translatable("screen.mmdlib_companion.lane_" + (i == 0 ? "a" : "b"))
.append(": ")
Expand Down
Loading
Loading