Machine UIs
When any machine that extends IMuiMachine or has a machine panel in its definition is right-clicked, it will attempt to open a UI.
Implementing IMuiMachine on a MetaMachine subclass is all that is needed to get a working panel:
public class MyMachine extends MetaMachine implements IMuiMachine {
public MyMachine(BlockEntityCreationInfo info) {
super(info);
}
}
Right-clicking the machine now opens a panel with a title bar and the player inventory attached. See Making a MUI2 Test Machine for a machine definition you can register to try this out.
How a machine panel is built
IMuiMachine splits UI creation into a few overridable steps. The order they run in determines
which one you want to override:
default ModularPanel<?> buildUI(PosGuiData data, PanelSyncManager syncManager, UISettings settings) {
var panelBuilder = getPanelBuilder(data, syncManager, settings); // (1)
panelBuilder.mainContents(parent -> buildMainUI(parent, data, syncManager, settings)); // (2)
return panelBuilder.build(syncManager, settings); // (3)
}
getPanelBuilderreturns aMachineUIPanelBuilder, which decides what the frame of the panel looks like: title bar, player inventory, configurator buttons.buildMainUIfills the content area in the middle of that frame. This is the method to override in almost every case.buildassembles everything into aMachineUIPanel.
Note
buildUI, buildMainUI and getPanelBuilder all run on both the client and the server. Any
value read from the machine has to be wrapped in a sync handler before a widget can display it.
See Sync Basics.
buildMainUI
buildMainUI receives the panel's main content area as a ParentWidget<?>, and adds children to it.
The content area defaults to MachineUIPanel.DEFAULT_CONTENT_WIDTH × DEFAULT_CONTENT_HEIGHT
(169 × 77) but will cover larger children.
@Override
public void buildMainUI(ParentWidget<?> mainWidget, PosGuiData guiData, PanelSyncManager syncManager,
UISettings settings) {
IntSyncValue bucketSyncer = new IntSyncValue(() -> cache.getFluidInTank(0).getAmount(), v -> {});
syncManager.syncValue("bucket_amount", bucketSyncer);
mainWidget.child(new ParentWidget<>()
.background(GTGuiTextures.DISPLAY)
.size(90, 63)
.center()
.child(Text.lang("gtceu.gui.fluid_amount").asWidget()
.color(0xffffff)
.margin(8, 0, 8, 0))
.child(Text.dynamic(() -> Component.literal(
FormattingUtil.formatBuckets(bucketSyncer.getIntValue())))
.asWidget()
.margin(8, 0, 20, 0)));
}
PumpMachine, FisherMachine and MinerMachine are good references for progressively more
complicated content areas.
getPanelBuilder
Override getPanelBuilder when you need to change the panel frame itself. MachineUIPanelBuilder
is a fluent builder with the following options:
| Method | Default | Effect |
|---|---|---|
attachInventory(boolean) |
true |
Attaches the player inventory below the content area. |
addTitleBar(boolean) |
true |
Draws the machine name title bar. |
drawGTLogo(boolean) |
false |
Draws a logo in the bottom right corner. |
gtLogoTexture(UITexture) |
GTGuiTextures.GREGTECH_LOGO |
The logo texture to use when drawGTLogo is enabled. |
addDefaultConfigurators(boolean) |
true |
Adds the power button, voiding button, distinct-buses button, batch mode button and recipe type button, when the machine supports them. |
addTraitConfigurators(boolean) |
true |
Lets attached traits implementing IAttachConfiguratorsTrait add their own configurators. |
leftConfigurators(Consumer<Flow>) |
no-op | Adds your own buttons to the left configurator column. |
rightConfigurators(Consumer<Flow>) |
no-op | Adds your own buttons to the right configurator column. |
A machine that wants an extra toggle button next to the stock ones:
@Override
public MachineUIPanelBuilder getPanelBuilder(PosGuiData data, PanelSyncManager syncManager,
UISettings settings) {
return MachineUIPanelBuilder.panelBuilder(this)
.rightConfigurators(configurators -> configurators
.child(new ToggleButton()
.value(new BoolValue.Dynamic(this::isJunkEnabled, this::setJunkEnabled))
.overlay(new ItemDrawable(Items.NAME_TAG))));
}
A machine that wants a bare panel with none of the stock buttons, as the primitive multiblocks do:
@Override
public MachineUIPanelBuilder getPanelBuilder(PosGuiData data, PanelSyncManager syncManager,
UISettings settings) {
return MachineUIPanelBuilder.panelBuilder(this)
.addDefaultConfigurators(false)
.addTraitConfigurators(false);
}
MachineUIPanelBuilder.defaultSteamMachinePanelBuilder(machine) is a shorthand for the above, and is
what SteamBoilerMachine uses.
Note
The configurator columns are drawn to the left and right of the machine panel. They hide themselves when they have no children, so a machine that adds no configurators shows neither column.
shouldOpenUI
Return false to suppress opening the UI for a particular interaction. The default implementation
always returns true. The block's onUse handling runs before this, so this hook only decides
whether the panel opens, not whether the click is consumed.
@Override
public boolean shouldOpenUI(Player player, InteractionHand hand, BlockHitResult hit) {
return isFormed();
}
Overriding buildUI directly
Override buildUI when you need more granular control outside what getPanelBuilder would allow you.
One way to do this is to reproduce the default three steps, then keep working with the returned panel like so:
@Override
public ModularPanel<?> buildUI(PosGuiData data, PanelSyncManager syncManager, UISettings settings) {
var panelBuilder = getPanelBuilder(data, syncManager, settings);
panelBuilder.mainContents(parent -> buildMainUI(parent, data, syncManager, settings));
var machinePanel = panelBuilder.build(syncManager, settings);
// ... attach extra widgets and panels to or make further changes to machinePanel here
return machinePanel;
}
If you skip getPanelBuilder/build entirely and construct a plain ModularPanel yourself, you
lose the title bar, player inventory and configurators. Only do that for a machine whose UI has
nothing in common with the standard machine layout.
Definition-level panels
A UI can also be attached to the machine definition rather than the machine class, with
MachineBuilder::ui. It takes a PanelFactory, which is the same thing as IMuiMachine except the
machine is handed to it as an argument instead of being this:
ModularPanel<?> buildUIFunction(PosGuiData data, PanelSyncManager syncManager, UISettings settings,
MetaMachine machine);
REGISTRATE.machine("my_machine", MyMachine::new)
.ui((data, syncManager, settings, machine) -> MachineUIPanelBuilder.panelBuilder(machine)
.mainContents(parent -> parent.child(Text.str("Hello").asWidget()))
.build(syncManager, settings))
.register();
This is how the shared singleblock recipe machine UI works: every simple machine gets
GTSingleblockMachinePanels.GENERAL_MACHINE, a PanelFactory that reads the machine's recipe type
UI layout and builds the slots from it. See Recipe Type UIs.
Warning
A definition-level UI takes precedence over IMuiMachine. If a machine has both, only the
definition's PanelFactory runs, and buildUI/buildMainUI on the machine are never called.
Related methods
tryToOpenUI(Player, InteractionHand, BlockHitResult)is called byMetaMachineBlock. It callsshouldOpenUIand then opens the panel throughMachineUIFactory. You normally do not override this.createScreen(PosGuiData, ModularPanel)is client-only, and returns theModularScreenwrapper. It defaults toGTGuiScreen. Override it only if you need a custom screen implementation.
Multiblock display text
Multiblock controllers do not build their status readout in buildMainUI. WorkableElectricMultiblockMachine
already overrides buildMainUI to draw the black display box in the middle of the panel, and fills it
from getWidgetsForDisplay(PanelSyncManager), a flat List<IWidget> rendered one entry per line.
The column that holds them is set to collapseDisabledChildren, so a disabled line takes up no space.
Give a widget a setEnabledIf to hide it without leaving a gap.
Adding lines from the definition
The usual way to add a line is additionalDisplay on the machine builder, which does not require a
custom machine class. It is a BiFunction<MultiblockControllerMachine, PanelSyncManager, List<IWidget>>:
public static final MultiblockMachineDefinition MY_MULTIBLOCK = REGISTRATE
.multiblock("my_multiblock", WorkableElectricMultiblockMachine::new)
// ... rotation, recipe types, pattern, model ...
.additionalDisplay((controller, syncManager) -> {
if (!(controller instanceof WorkableElectricMultiblockMachine machine))
return Collections.emptyList();
BooleanSyncValue isFormed = syncManager.getOrCreateSyncHandler("isFormed",
BooleanSyncValue.class,
() -> new BooleanSyncValue(controller::isFormed));
IntSyncValue tier = syncManager.getOrCreateSyncHandler("machineTier", IntSyncValue.class,
() -> new IntSyncValue(machine::getTier));
return List.of(Text
.dynamic(() -> Component.translatable("my_addon.multiblock.my_multiblock.tier",
GTValues.VNF[tier.getIntValue()]))
.asWidget()
.setEnabledIf(w -> isFormed.getBoolValue()));
})
.register();
Most real uses need the same things this example does:
- The function is handed a
MultiblockControllerMachine, which has no tier, no recipe logic and no coils. Anything more specific needs aninstanceofcheck and anemptyList()bail-out.getTier()above comes fromITieredMachine, not from the base class. getWidgetsForDisplayruns on both the client and the server, and the controller's fields are only meaningful on the server, so anything the text depends on goes through a sync handler. UsegetOrCreateSyncHandlerto reuse a handler if another line already made one under that name.Text.langandText.strare evaluated once when the panel is built, whileText.dynamicre-evaluates while it is open. UseText.dynamicfor text that changes.- Most status text is meaningless for an unformed structure, so guard it with
setEnabledIf.
Adding the same line from the machine class
If the multiblock already has its own class, override getWidgetsForDisplay instead. The same line as
above becomes:
public class MyMultiblockMachine extends WorkableElectricMultiblockMachine {
@Override
public List<IWidget> getWidgetsForDisplay(PanelSyncManager syncManager) {
List<IWidget> widgets = super.getWidgetsForDisplay(syncManager);
BooleanSyncValue isFormed = syncManager.getOrCreateSyncHandler("isFormed",
BooleanSyncValue.class,
() -> new BooleanSyncValue(this::isFormed));
IntSyncValue tier = syncManager.getOrCreateSyncHandler("machineTier", IntSyncValue.class,
() -> new IntSyncValue(this::getTier));
widgets.add(Text
.dynamic(() -> Component.translatable("my_addon.multiblock.my_multiblock.tier",
GTValues.VNF[tier.getIntValue()]))
.asWidget()
.setEnabledIf(w -> isFormed.getBoolValue()));
return widgets;
}
}
The syncing, Text.dynamic and setEnabledIf points above apply here too. The main difference is
that you call super.getWidgetsForDisplay first and add to what it returns. Dropping the super
call replaces the entire readout instead of adding to it. Because you own the list here, you can also
insert a line at a specific position or remove a default one.
Where the lines end up
Lines added through additionalDisplay are inserted part-way down the standard list rather than
appended to the end of it, below the machine's core status lines and above the parallel and output
ones. The exact position is decided by WorkableMultiblockMachine.getWidgetsForDisplay.