Creating a Computer Program
The API for computer programs is intentionly simple and very open ended. The API only offers to ability to register your program and provide a simple GUI interface on the client. It is up to you to write your own code if you want to do anything advanced, like retrieving data from the server side with packets. - MrCrayfish
Create the Program
Creating a program begins with extending the class Program
. When you open the program on the Computer, your custom class will be instantiated on both the client and server. The program class is where all your common logic should be written.
import com.mrcrayfish.furniture.refurbished.computer.Program;
import com.mrcrayfish.furniture.refurbished.blockentity.IComputer;
// Blank program
public class ExampleProgram extends Program
{
public ExampleProgram(ResourceLocation id, IComputer computer)
{
super(id, computer);
}
}
You may have noticed that Program
takes in the constructor arguments ResourceLocation
and IComputer
. The ResourceLocation
is simply the same id you used when you registered it using Computer#installProgram
. The more interesting argument is the IComputer
interface. This will give you access to potentially useful data like an instance of the Player
currently using the Computer, the BlockPos
of the Computer block, the ability to check if program is server side. You can access to IComputer
by simply calling Program#getComputer()
anywhere in your program code.
Registering
You will need to register the program class in your common setup with the following code:
Computer computer = Computer.get();
computer.installProgram(new ResourceLocation("<mod_id>", "<program_id>"), ExampleProgram::new);
Additional Methods
Program
also comes with methods you can override in your program class to write your logic.
tick()
- Called every tick when you program is running.onClose()
- Called when your program is closed, either by thePlayer
closing the window or exiting the GUI.
Displaying your Program
Assuming you have followed all the steps correctly in the guide so far, you will notice that your program will be available in the Computer but you will not launch it, nor does it have an icon. This is because a program needs to be bound to a DisplayableProgram
instance, and the icon sheet cannot be located.
Setting the Icon
The icon for your program has been automatically managed for you, but is a little trival. An icon sheet is automatically generated based on your mod_id
at the asset location mod_id:textures/gui/program_icons.png
. You will need to create a PNG
file with the exact size of 128
by 128
. Icons are strictly 16x
and start at the top left of the sheet. The index of your icon is based on the order your register your programs. So your first program will start at the pixel coordinates (0, 0)
, your second program will start at the pixel coordinates (16, 0)
, and so on. This sheet is to avoid registering lots of textures.
Below is template you can save, and it also includes the colour palette if you wish to match with the theme of the computer.
Binding the Displayable
Much like you would bind a MenuType
to an AbstractContainerScreen
, we need to bind a Program
to a DisplayableProgram
. DisplayableProgram
is a client side only class used for writing all your UI code (like adding widgets) and client logic. Upon extending DisplayableProgram
, you will need to provide a generic argument, which should be your custom Program
class.
The constructor will need to accept the program in the arguments. This is required for DisplayableProgram
constructor, but also gives you access to the client instance of your program. Additonally this is also where you set the width
and height
of your program. It should not be greater than 224x108
, otherwise an error will be thrown.
import com.mrcrayfish.furniture.refurbished.computer.client.DisplayableProgram;
public class ExampleGraphics extends DisplayableProgram<ExampleProgram>
{
public ExampleGraphics(ExampleProgram program)
{
super(program, 100, 100);
}
}
Finally to actually bind the program, you will need to call the following in your client setup:
Display.get().bind(ExampleProgram.class, ExampleGraphics::new);
Scenes
A DisplayableProgram
requires that you give it a Scene
. Scenes are the core of drawing and orgnaising your interface. A scene holds your widgets and custom rendering code. For example, a chat application you may have a different scene for contacts list and settings menu since bundling them in the same scene would get messy.
For easy management, scenes should be created as inner classes. Immediately in the constructor of your DisplayableProgram
, you should set the Scene
to display on initial load of your program. By default, scenes do not require any special constructor but you may want to pass your DisplayableProgram
so the scene has access to your Program
instance and more (since you'll probably want it).
import com.mrcrayfish.furniture.refurbished.computer.client.Scene;
public class ExampleGraphics extends DisplayableProgram<ExampleProgram>
{
public ExampleGraphics(ExampleProgram program)
{
super(program, 100, 100);
// Sets the scene to show on intial load
this.setScene(new Main(this));
}
// The main scene
public static class Main extends Scene
{
private final ExampleGraphics graphics;
public Home(ExampleGraphics graphics)
{
this.graphics = graphics;
}
}
}
Adding and Updating Widgets
Scenes accept any GuiEventListener
, essentially any widget you could add to a Screen
. You can add a widget to your scene by calling the Scene#addWidget
method in the constructor of your scene. Under the hood of scenes, events will automatically be passed to your widgets and drawn for you.
public static class Main extends Scene
{
private final ExampleGraphics graphics;
private final Button testBtn;
public Home(ExampleGraphics graphics)
{
this.graphics = graphics;
this.testBtn = this.addWidget(Button.builder(Component.literal("Test"), button -> {
System.out.println("I pressed the test button");
}).size(50, 20).build());
}
}
You may have noticed that no position was set for widget. This is because the position of widgets should be set/updated by inside the updateWidgets()
method. You will need to override this method in your class.
public static class Main extends Scene
{
...
// All content is positioned from the top left, just like regular Screens
@Override
public void updateWidgets(int contentStart, int contentTop)
{
this.testBtn.setPosition(contentStart + 5, contentTop + 5);
}
}
Custom Rendering
Of course adding widgets is only one half of what you need to completely draw your scene. You may want to provide custom rendering. This will give you access to vanilla's GuiGraphics
so you can fill
any colour or blit
any sprites into the scene.
An important thing to note is that everything drawn is relative, not absolute. So drawing at the position (0, 0)
is the top left of your scene, not the top left of the game window (when compared to Screen#render
). Below is an example of filling the entire background of the scene with a colour.
The drawing of your program is wrapped with a call to OpenGL scissor test. This means content drawn outside of the width and height of your program will not be visible.
public static class Main extends Scene
{
...
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick)
{
// Fills the entire background with a colour. Width and height of scene is
// obtained via DisplayableProgram instance.
graphics.fill(0, 0, this.graphics.getWidth(), this.graphics.getHeight(), 0xFF262626);
}
}
Switching Scenes
A common action you would want to run is changing the Scene
after clicking a button widget. This is done via the same DisplayableProgram#setScene
call that is run on the initial load of the DisplayableProgram
. In your scene, assuming you have passed an instance of your DisplayableProgram
to it, simply just call this.graphics.setScene(...)
inside your widget handler. From the above example, test button will now look like this:
this.testBtn = this.addWidget(Button.builder(Component.literal("Test"), button -> {
graphics.setScene(new DifferentScene(graphics));
}).size(50, 20).build());
Custom Window Styling
DisplayableProgram
has the ability to control the styling of the window that it is contained within. Below is the list of functions you can call in the constructor to change the styling. All methods accept a decimal colour (e.g. 0xAARRGGBB
)
setWindowOutlineColour(int)
- Sets the outline colour of the windowsetWindowTitleBarColour(int)
- Sets the title bar colour of the windowsetWindowTitleLabelColour(int)
- Sets the colour of the title bar label of the windowsetWindowBackgroundColour(int)
- Sets the background colour of the window
Final Words
That's it. A very basic guide to creating a program. It is up to you to figure out the rest. Of course, feel free to read the source code of programs already in the mod. The relavent source code can be found here.