This documentation about the GUI system was written for developers with Minecraft 1.18.1 in mind. Much of it should still apply to older versions (excluding 1.15.2), but class names and locations will likely be different.
Phosphophyllite Screens are Phosphophyllite's own GUI system for blocks/tiles, based upon Minecraft's internal system. This is included in Phosphophyllite, but modders using Phosphophyllite are not required to use it and can safely ignore it if desired.
The system is primarily intended to make writing “container'd” blocks easier; that is to say, blocks or tiles that make use of a container or menu to keep track of data, such as a Furnace or Cyanite Reprocessor. For GUIs that do not use a container, such as a settings menu or error screen, it is recommended to use Minecraft's internal system, as ignoring containers makes the vanilla system a lot cleaner to work with.
Below is a generalized overview behind some of the things in the GUI system.
Screens in Phosphophyllite (the rendered, final output displayed to the user) all extend the PhosphophylliteScreen
class. When extending, you must specify your container's class as a generic. Inside of the constructor, you will need to pass in the following information:
container
: An instance of your container.playerInventory
: The player's current inventory.title
: The name of your screen.textureAtlas
: The texture for your screen. The atlas is usually 256 × 256 pixels in size.width
, height
: The size of the texture for your screen. The texture itself is usually less than 256 × 256 pixels in size.This populated constructor is all you need to render a simple screen. The final thing to do is to register your screens and their associated containers in the FMLClientSetupEvent
phase, by calling MenuScreens.register()
with an instance of your container, and the constructor for your screen.
That simple screen is good for static images. However, it can't do much on its own. Most of the time, you'll want to update the screen depending on the state of a tile entity, or send a command/callback from your GUI back to the tile entity. This is where containers and the GuiSync
class come into play.
Unlike screens, Phosphophyllite doesn't have a dedicated PhosphophylliteContainer
class. Instead, you should extend Minecraft's AbstractContainerMenu
class and implement Phosphophyllite's GuiSync.IGUIPacketProvider
. The latter will require overriding of the function getGuiPacket()
, which is what is used to communicate the current state of the container to the screen. In practice, an easy way to populate this is to store a GUI state object inside your tile entity, which contains all information relevant to the GUI. Whenever the screen wants to update its information, it will call this function, and the packet returned from the function will be automatically synchronized between the server and client.
Lastly, registration for containers can be handled through Phosphophyllite's registry system. This involves doing three things:
@RegisterContainer(name = “unique_container_name”)
. Your container name must be unique, or the game will fail to load.@RegisterContainer.Type
annotation. The name of the field doesn't matter, but the type should be MenuType<MyContainerClass>
, filling in whatever your container class is named.@RegisterContainer.Supplier
annotation. Again, the name of the field doesnt matter, but it should have the type ContainerSupplier
, and should be initialized with a supplier. If your container class's constructor has arguments matching those in ContainerSupplier
(that is: int, BlockPos, Player
), then you can simply put MyContainerClass::new
.The rest of the container may be populated as needed for your use case.
Now that our data is synchronized, we can begin doing things with it. Inside your screen class, you can create a private field to store the last known state of the container. Inside your constructor, you can initialize your state field by calling this.getMenu().getGuiPacket()
and casting the returned value to your state class. Afterwards, you can render your GUI however you like, using the data inside that field.
But what if the data is updated? Our field currently will check the tile entity's state once at the very beginning, and then ignore it. For some use cases, that's fine. If you would like for your GUI to have a constant feed of accurate data though, you should override the containerTick()
method, and perform the same step as above. Don't forget, however, to also call the original container ticking method (that is to say, super.containerTick()
)!
In addition to sending data from the server to the client, we can also send commands back from the client to the server. This is particularly useful if your GUI is controlling something like machinery, where it would be desirable to add an on/off switch or something similar. We can use executeRequest()
and runRequest()
to accomplish this.
To start, we need to override the executeRequest()
function inside your container class. Inside of this, you can do whatever side and validity checking you please, but it is generally advised to check that the current level is not null somehow (such as if the world was closed mid-request). Most importantly, however, you should check the current side of the tile entity. If it is client side, you should call runRequest()
, passing in your request name and data – this call should refer to that of GuiSync, not your tile entity (which I'll explain in a second). If the is not client side, you should call myTileEntity.runRequest()
with your request name and data.
You may wonder why the distinction. Primarily, if the call to executeRequest()
was called on the client side, we want to pass it to GuiSync
to send it to the server. However, if it is not on the client side, we want to send it to the tile entity so that it may handle it. An example of an executeRequest(String, Object) is provided below, since the above explanation might be a little confusing:
@Override
public void executeRequest(String requestName, Object requestData) {
assert tileEntity.getLevel() != null;
if (tileEntity.getLevel().isClientSide) {
runRequest(requestName, requestData);
return;
}
tileEntity.runRequest(requestName, requestData);
}
Inside your tile entity, you can now implement a runRequest()
function. This doesn't actually have to be called runRequest
, but whatever you call it, it should match the last line of the example below. Afterwards, you should have a working request system.
A common way to use this is an InteractiveElement
(explained later) in your screen, with it's callback set to call executeRequest
with a request name and a bit of information, such as a boolean or integer. Then, inside your tile entity, you can read this request name, and cast the information according to what is expected, and do whatever it is you need. For example, say your screen has a button (or some other InteractiveElement
) named powerSwitch
that is designed to toggle something on or off:
powerSwitch.onMouseReleased = (mX, mY, btn) -> {
screen.getMenu().executeRequest("togglePowerSwitch", powerSwitch.getState());
return true;
};
Assuming your container is setup correctly, your tile entity could have the following:
public void runRequest(String requestName, @Nullable Object requestData) {
switch (requestName) {
// Toggle the power switch.
case "togglePowerSwitch" -> {
if (!(requestData instanceof Boolean)) {
// Power switch state should be true or false, so the data must not be right.
return;
}
this.setPowerStatus((Boolean) requestData);
}
// [ other cases below ] //
In Phosphophyllite, screens can be customized using screen elements. These are similar to vanilla's widgets, but implemented in a different way. While you can use both, Phosphophyllite screens are intended to work with screen elements, and as such vanilla widgets may or may not render correctly or function at all.
To add a widget to a Phosphophyllite screen, all you need to do in your screen is call addScreenElement()
with an instance of your element.
Currently, Phosphophyllite comes with three widgets by default, and an abstract widget upon which to build your own. Starting with the most basic element, and working up in complexity:
AbstractElement
: The base element used by all other elements in Phosphophyllite's GUI system. There's not much to these.TooltipElement
: The simplest usable element, this allows you to define an area that will show a tooltip when hovered over. This is useful when you have a GUI texture with symbols already on it, and would like to add tooltip functionality without chopping up your texture and stitching a RenderedElement
on to it.RenderedElement
: This element is the first visible element. This allows you to render a section using a texture elsewhere in your GUI's texture, ranging from things like an animated picture to a fully animated water tank gauge. It builds on the TooltipElement
, and as such can display tooltips on hover.InteractiveElement
: The most complex element, this element has the traits from TooltipElement
and RenderedElement
. This element allows direct interaction with the user, through the normal GUI events you would receive normally from vanilla's GuiEventListener
. This can be used to create things ranging from simple buttons, to full text entry boxes.Below is an example of an InteractiveElement, which is positioned on-screen at (100, 30) and a size of 16 × 16 pixels, and a UV coordinate of (116, 0). This will play a noise when clicked and change texture when hovered over:
// Create an instance of the element.
InteractiveElement<MyCoolContainer> clickySoundButton =
new InteractiveElement<>(this, 100, 30, 16, 16, 116, 0, new TextComponent("Look Ma, I'm in a tooltip!"));
// Create the callback to play the sound.
textBoxEnterButton.onMouseReleased = (mX, mY, btn) -> {
// Check that the mouse was over the button when it clicked.
if (textBoxEnterButton.isMouseOver(mX, mY)) {
// Play the selection sound.
textBoxEnterButton.playSound(SoundEvents.UI_BUTTON_CLICK);
return true;
} else {
// It wasn't hovered, don't do the thing.
return false;
}
};
// Create the custom rendering logic.
textBoxEnterButton.onRender = ((mS, mX, mY) -> {
// Check where the mouse is.
if (textBoxEnterButton.isMouseOver(mX, mY)) {
// Mouse is hovering, highlight the button.
textBoxEnterButton.blit(mS, 132, 0);
} else {
// It isn't hovered, don't highlight the button.
textBoxEnterButton.blit(mS, 116, 0);
}
});
More examples can be seen in Bigger Reactors, which makes extensive use of this system for all of its GUIs.