Merge pull request #2933 from Voxelstice/autocal-browse-fix

AUTOCAL "Desktop.browse" Linux function fix
This commit is contained in:
HbmMods 2026-06-16 16:41:43 +02:00 committed by GitHub
commit bb688e2f4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,434 +1,452 @@
package com.hbm.inventory.gui; package com.hbm.inventory.gui;
import java.awt.Desktop; import java.awt.Desktop;
import java.io.File; import java.io.File;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.nio.charset.StandardCharsets; import java.io.IOException;
import java.nio.file.Files; import java.nio.charset.StandardCharsets;
import java.nio.file.Paths; import java.nio.file.Files;
import java.util.Arrays; import java.nio.file.Paths;
import java.net.URI;
import org.lwjgl.opengl.GL11; import java.util.Arrays;
import com.hbm.lib.RefStrings; import org.lwjgl.Sys;
import com.hbm.main.MainRegistry; import org.lwjgl.opengl.GL11;
import com.hbm.packet.PacketDispatcher;
import com.hbm.packet.toserver.NBTControlPacket; import com.hbm.lib.RefStrings;
import com.hbm.tileentity.network.TileEntityRadioAUTOCAL; import com.hbm.main.MainRegistry;
import com.hbm.packet.PacketDispatcher;
import net.minecraft.client.Minecraft; import com.hbm.packet.toserver.NBTControlPacket;
import net.minecraft.client.audio.PositionedSoundRecord; import com.hbm.tileentity.network.TileEntityRadioAUTOCAL;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.nbt.NBTTagCompound; import net.minecraft.client.Minecraft;
import net.minecraft.util.EnumChatFormatting; import net.minecraft.client.audio.PositionedSoundRecord;
import net.minecraft.util.ResourceLocation; import net.minecraft.client.gui.GuiScreen;
import net.minecraft.nbt.NBTTagCompound;
public class GUIScreenRadioAUTOCAL extends GuiScreen { import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.ResourceLocation;
protected static final ResourceLocation texture = new ResourceLocation(RefStrings.MODID + ":textures/gui/machine/gui_rtty_autocal.png");
protected TileEntityRadioAUTOCAL autocal; public class GUIScreenRadioAUTOCAL extends GuiScreen {
protected int xSize = 170; protected static final ResourceLocation texture = new ResourceLocation(RefStrings.MODID + ":textures/gui/machine/gui_rtty_autocal.png");
protected int ySize = 138; protected TileEntityRadioAUTOCAL autocal;
protected int guiLeft;
protected int guiTop; protected int xSize = 170;
protected int ySize = 138;
public GUIScreenRadioAUTOCAL(TileEntityRadioAUTOCAL autocal) { protected int guiLeft;
this.autocal = autocal; protected int guiTop;
}
public GUIScreenRadioAUTOCAL(TileEntityRadioAUTOCAL autocal) {
@Override this.autocal = autocal;
public void initGui() { }
super.initGui();
this.guiLeft = (this.width - this.xSize) / 2; private void browse(URI uri) throws IOException {
this.guiTop = (this.height - this.ySize) / 2; // workaround for Java not supporting all platforms, mostly for Linux
} if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(uri);
@Override } else {
public void drawScreen(int mouseX, int mouseY, float f) { if (Sys.getVersion().charAt(0) == '3') {
this.drawDefaultBackground(); // probably a LWJGL3ify user, open the folder instead since that somehow seems to work
this.drawGuiContainerBackgroundLayer(f, mouseX, mouseY); File uploadFolder = new File(MainRegistry.configDir.getParentFile(), "hbmComputerUpload");
GL11.glDisable(GL11.GL_LIGHTING); if (uploadFolder.exists()) Sys.openURL(uploadFolder.toString());
this.drawGuiContainerForegroundLayer(mouseX, mouseY); } else {
GL11.glEnable(GL11.GL_LIGHTING); Sys.openURL(uri.toString());
} }
}
@Override }
protected void mouseClicked(int x, int y, int i) {
super.mouseClicked(x, y, i); @Override
public void initGui() {
NBTTagCompound data = null; super.initGui();
this.guiLeft = (this.width - this.xSize) / 2;
if(checkClick(x, y, 8, 36, 18, 18)) { data = new NBTTagCompound(); data.setBoolean("on", true); } this.guiTop = (this.height - this.ySize) / 2;
if(checkClick(x, y, 28, 36, 18, 18)) { data = new NBTTagCompound(); data.setBoolean("ignore", true); } }
if(checkClick(x, y, 48, 36, 18, 18)) { data = new NBTTagCompound(); data.setBoolean("auto", true); }
@Override
// open folder and generate new script file public void drawScreen(int mouseX, int mouseY, float f) {
if(checkClick(x, y, 104, 36, 18, 18)) { this.drawDefaultBackground();
try { this.drawGuiContainerBackgroundLayer(f, mouseX, mouseY);
File uploadFolder = new File(MainRegistry.configDir.getParentFile(), "hbmComputerUpload"); GL11.glDisable(GL11.GL_LIGHTING);
File script = new File(uploadFolder, "script.txt"); this.drawGuiContainerForegroundLayer(mouseX, mouseY);
if(!uploadFolder.exists()) uploadFolder.mkdir(); GL11.glEnable(GL11.GL_LIGHTING);
if(!script.exists()) script.createNewFile(); }
script.setExecutable(false);
Desktop.getDesktop().browse(script.toURI()); @Override
} catch(Throwable ex) { MainRegistry.logger.error("Couldn't open link", ex); } protected void mouseClicked(int x, int y, int i) {
} super.mouseClicked(x, y, i);
// open folder and generate new doc file NBTTagCompound data = null;
if(checkClick(x, y, 144, 36, 18, 18)) {
try { if(checkClick(x, y, 8, 36, 18, 18)) { data = new NBTTagCompound(); data.setBoolean("on", true); }
File uploadFolder = new File(MainRegistry.configDir.getParentFile(), "hbmComputerUpload"); if(checkClick(x, y, 28, 36, 18, 18)) { data = new NBTTagCompound(); data.setBoolean("ignore", true); }
File doc = new File(uploadFolder, "documentation.md"); if(checkClick(x, y, 48, 36, 18, 18)) { data = new NBTTagCompound(); data.setBoolean("auto", true); }
if(!uploadFolder.exists()) uploadFolder.mkdir();
if(!doc.exists()) { // open folder and generate new script file
doc.createNewFile(); if(checkClick(x, y, 104, 36, 18, 18)) {
try { try {
PrintWriter printer = new PrintWriter(doc, StandardCharsets.US_ASCII.name()); File uploadFolder = new File(MainRegistry.configDir.getParentFile(), "hbmComputerUpload");
for(String line : DOCS) printer.println(line); File script = new File(uploadFolder, "script.txt");
printer.close(); if(!uploadFolder.exists()) uploadFolder.mkdir();
} catch(Throwable e) { } if(!script.exists()) script.createNewFile();
} script.setExecutable(false);
Desktop.getDesktop().browse(doc.toURI()); browse(script.toURI());
} catch(Throwable ex) { MainRegistry.logger.error("Couldn't open link", ex); } } catch(Throwable ex) { MainRegistry.logger.error("Couldn't open link", ex); }
} }
if(checkClick(x, y, 84, 36, 18, 18)) { // open folder and generate new doc file
try { if(checkClick(x, y, 144, 36, 18, 18)) {
File uploadFolder = new File(MainRegistry.configDir.getParentFile(), "hbmComputerUpload"); try {
File script = new File(uploadFolder, "script.txt"); File uploadFolder = new File(MainRegistry.configDir.getParentFile(), "hbmComputerUpload");
if(!uploadFolder.exists()) uploadFolder.mkdir(); File doc = new File(uploadFolder, "documentation.md");
if(!script.exists()) { if(!uploadFolder.exists()) uploadFolder.mkdir();
script.createNewFile(); if(!doc.exists()) {
script.setExecutable(false); doc.createNewFile();
return; try {
} PrintWriter printer = new PrintWriter(doc, StandardCharsets.US_ASCII.name());
/*FileReader reader = new FileReader(script); for(String line : DOCS) printer.println(line);
BufferedReader buffer = new BufferedReader(reader); printer.close();
String[] lines = buffer.lines().toArray(String[]::new); } catch(Throwable e) { }
buffer.close(); }
// this is going to blow the fuck up once we hit the max packet size, but let's ignore that for now browse(doc.toURI());
data = new NBTTagCompound(); } catch(Throwable ex) { MainRegistry.logger.error("Couldn't open link", ex); }
StringBuilder builder = new StringBuilder(); }
for(int l = 0; l < lines.length; l++) {
builder.append(lines[i]); if(checkClick(x, y, 84, 36, 18, 18)) {
if(l < lines.length - 1) builder.append("\n"); // yeah why the fuck not try {
} File uploadFolder = new File(MainRegistry.configDir.getParentFile(), "hbmComputerUpload");
data.setString("payload", builder.toString());*/ File script = new File(uploadFolder, "script.txt");
byte[] bytes = Files.readAllBytes(Paths.get(script.toURI())); if(!uploadFolder.exists()) uploadFolder.mkdir();
data = new NBTTagCompound(); if(!script.exists()) {
data.setString("payload", new String(bytes, StandardCharsets.UTF_8)); script.createNewFile();
script.setExecutable(false);
} catch(Throwable ex) { } return;
} }
/*FileReader reader = new FileReader(script);
// this thing can both upload and download files so let's be careful about this BufferedReader buffer = new BufferedReader(reader);
// the upload is simple, it's just text that is handled by the AUTOCAL, so doing anything malicious isn't more likely than with any other package String[] lines = buffer.lines().toArray(String[]::new);
// download is iffy, because we take text from the server, fully user-definable, and save it to disk. it's stored as a txt so accudentally running it buffer.close();
// or getting it to run itself, should it be a malicious script, is unlikely. still, we want to minimized the chances as much as we can // this is going to blow the fuck up once we hit the max packet size, but let's ignore that for now
// option 1: set file attribute to disallow running (i.e. disable executable perm) data = new NBTTagCompound();
// option 2: add fluff that would break scripts, however they might work. we can't change the actual lines because we want the script to be edited, StringBuilder builder = new StringBuilder();
// but we can add some extra crap that would either halt common scripting langs entirely or at least disrupt them into not functioning for(int l = 0; l < lines.length; l++) {
// option 3: enforce validation so only MS-ES1 script can be received by the client. this means that info such as comments or incorrectly written commands builder.append(lines[i]);
// are lost, however this is the safest way because it becomes impossible to send malicious code, but it also interferes with regular user operation more if(l < lines.length - 1) builder.append("\n"); // yeah why the fuck not
}
if(data != null) { data.setString("payload", builder.toString());*/
mc.getSoundHandler().playSound(PositionedSoundRecord.func_147674_a(new ResourceLocation("gui.button.press"), 1.0F)); byte[] bytes = Files.readAllBytes(Paths.get(script.toURI()));
PacketDispatcher.wrapper.sendToServer(new NBTControlPacket(data, autocal.xCoord, autocal.yCoord, autocal.zCoord)); data = new NBTTagCompound();
} data.setString("payload", new String(bytes, StandardCharsets.UTF_8));
}
} catch(Throwable ex) { }
protected boolean checkClick(int x, int y, int left, int top, int sizeX, int sizeY) { }
return guiLeft + left <= x && guiLeft + left + sizeX > x && guiTop + top < y && guiTop + top + sizeY >= y;
} // this thing can both upload and download files so let's be careful about this
// the upload is simple, it's just text that is handled by the AUTOCAL, so doing anything malicious isn't more likely than with any other package
private void drawGuiContainerForegroundLayer(int x, int y) { // download is iffy, because we take text from the server, fully user-definable, and save it to disk. it's stored as a txt so accudentally running it
// or getting it to run itself, should it be a malicious script, is unlikely. still, we want to minimized the chances as much as we can
for(int i = 0; i < autocal.history.length; i++) { // option 1: set file attribute to disallow running (i.e. disable executable perm)
String line = autocal.history[i]; // option 2: add fluff that would break scripts, however they might work. we can't change the actual lines because we want the script to be edited,
if(line == null || line.isEmpty()) continue; // but we can add some extra crap that would either halt common scripting langs entirely or at least disrupt them into not functioning
this.fontRendererObj.drawString(line, guiLeft + 7, guiTop + 73 + i * 10, 0x00ff00); // option 3: enforce validation so only MS-ES1 script can be received by the client. this means that info such as comments or incorrectly written commands
} // are lost, however this is the safest way because it becomes impossible to send malicious code, but it also interferes with regular user operation more
if(checkClick(x, y, 8, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.RED + "ON/OFF"}), x, y); if(data != null) {
if(checkClick(x, y, 28, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.RED + "Ignore Errors", "Skips instructions that error,", "leaving the computer turned on.", "May cause unintended behavior", "and inconsistencies."}), x, y); mc.getSoundHandler().playSound(PositionedSoundRecord.func_147674_a(new ResourceLocation("gui.button.press"), 1.0F));
if(checkClick(x, y, 48, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.RED + "Automatic Reboot", "Restarts the computer automatically when", "the program stops due to an error", "or after finishing."}), x, y); PacketDispatcher.wrapper.sendToServer(new NBTControlPacket(data, autocal.xCoord, autocal.yCoord, autocal.zCoord));
}
if(checkClick(x, y, 84, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.BLUE + "Upload Program"}), x, y); }
if(checkClick(x, y, 104, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.BLUE + "Open Program File"}), x, y);
if(checkClick(x, y, 124, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.BLUE + "Download Program", EnumChatFormatting.RED + "Currently unsupported!"}), x, y); protected boolean checkClick(int x, int y, int left, int top, int sizeX, int sizeY) {
if(checkClick(x, y, 144, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.BLUE + "Open Documentation"}), x, y); return guiLeft + left <= x && guiLeft + left + sizeX > x && guiTop + top < y && guiTop + top + sizeY >= y;
} }
private void drawGuiContainerBackgroundLayer(float f, int mouseX, int mouseY) { private void drawGuiContainerForegroundLayer(int x, int y) {
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
Minecraft.getMinecraft().getTextureManager().bindTexture(texture); for(int i = 0; i < autocal.history.length; i++) {
drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); String line = autocal.history[i];
if(line == null || line.isEmpty()) continue;
if(autocal.isOn) drawTexturedModalRect(guiLeft + 8, guiTop + 36, xSize, 0, 18, 18); this.fontRendererObj.drawString(line, guiLeft + 7, guiTop + 73 + i * 10, 0x00ff00);
if(!autocal.ignoreError) drawTexturedModalRect(guiLeft + 28, guiTop + 36, xSize, 18, 18, 18); }
if(!autocal.autoReboot) drawTexturedModalRect(guiLeft + 48, guiTop + 36, xSize, 36, 18, 18);
} if(checkClick(x, y, 8, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.RED + "ON/OFF"}), x, y);
if(checkClick(x, y, 28, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.RED + "Ignore Errors", "Skips instructions that error,", "leaving the computer turned on.", "May cause unintended behavior", "and inconsistencies."}), x, y);
@Override if(checkClick(x, y, 48, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.RED + "Automatic Reboot", "Restarts the computer automatically when", "the program stops due to an error", "or after finishing."}), x, y);
protected void keyTyped(char c, int b) {
if(b == 1 || b == Minecraft.getMinecraft().gameSettings.keyBindInventory.getKeyCode()) { if(checkClick(x, y, 84, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.BLUE + "Upload Program"}), x, y);
Minecraft.getMinecraft().thePlayer.closeScreen(); if(checkClick(x, y, 104, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.BLUE + "Open Program File"}), x, y);
} if(checkClick(x, y, 124, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.BLUE + "Download Program", EnumChatFormatting.RED + "Currently unsupported!"}), x, y);
} if(checkClick(x, y, 144, 36, 18, 18)) this.func_146283_a(Arrays.asList(new String[] {EnumChatFormatting.BLUE + "Open Documentation"}), x, y);
}
@Override public boolean doesGuiPauseGame() { return false; }
private void drawGuiContainerBackgroundLayer(float f, int mouseX, int mouseY) {
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
public static final String[] DOCS = new String[] { Minecraft.getMinecraft().getTextureManager().bindTexture(texture);
"# AUTOCAL - The Automatic Calculator", drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize);
"",
"## About this document", if(autocal.isOn) drawTexturedModalRect(guiLeft + 8, guiTop + 36, xSize, 0, 18, 18);
"This documentation is designed to be understandable even by people with no programming background. This documentation extends to the AUTOCAL unit as well as the MS-ES1 script language it is programmed with.", if(!autocal.ignoreError) drawTexturedModalRect(guiLeft + 28, guiTop + 36, xSize, 18, 18, 18);
"", if(!autocal.autoReboot) drawTexturedModalRect(guiLeft + 48, guiTop + 36, xSize, 36, 18, 18);
"Read this document carefully as all the described concepts are vital for using the AUTOCAL unit.", }
"",
"## About AUTOCAL", @Override
"The AUTOCAL automatic calculator is a basic machine that reads a script, line by line, and performs actions based on those lines. This means it can be programmed with behavior, doing math, and handling signals. It communicated with the outside world exclusively using Redstone-over-Radio (RoR) signals, allowing RoR to supply it with values to be processed, and returning RoR signals to be displayed or for controlling RoR receiving devices.", protected void keyTyped(char c, int b) {
"", if(b == 1 || b == Minecraft.getMinecraft().gameSettings.keyBindInventory.getKeyCode()) {
"The first button is the on/off switch. Turning the AUTOCAL unit on will start the script at the first line. If the script concludes, an error is encountered (without the \"ignore errors\" setting enabled), the `shutdown` command is used or no program is loaded to begin with, the unit will automatically power down.", Minecraft.getMinecraft().thePlayer.closeScreen();
"", }
"The second button is the \"ignore errors\" setting. The red X means the setting is disabled, if an error is encountered in the program (an unrecognized command, missing or incorrect parameters, anything the script isn't meant to handle) then the unit will shut off. Otherwise, the erroring instruction is simply skipped. This may cause undefined or unexpected behavior, however in certain circumstances this doesn't matter. It's advised to keep this turned off, as it makes it easier to find obvious errors in the script.", }
"",
"The third button is the \"automatic reboot\" setting. If the unit is powered down, no matter the reason, it will automatically try to start again. This allows for a simple \"loop\" where the same script is repeated every time it ends by simply rebooting the AUTOCAL unit at the end. Do note that doing so will delete all saved variables, more on how variables work later.", @Override public boolean doesGuiPauseGame() { return false; }
"",
"The fourth button is for uploading the script. The script is in the minecraft's install folder, under the `hbmComputerUpload` folder, named `script.txt`. Clicking this button will send the contents of this script file to the AUTOCAL unit.",
"", public static final String[] DOCS = new String[] {
"The fifth button is for opening the script file. If no script file exists yet, the folder and empty script will be created.", "# AUTOCAL - The Automatic Calculator",
"", "",
"The sixth button is for downloading a script file. The existing `script.txt` is deleted and replaced with the script that is loaded onto the AUTOCAL unit.", "## About this document",
"", "This documentation is designed to be understandable even by people with no programming background. This documentation extends to the AUTOCAL unit as well as the MS-ES1 script language it is programmed with.",
"The simple workflow of programming an AUTOCAL unit is therefore using button #5 to open the file, writing the program, using #4 to flash the program to the AUTOCAL unit, and then using the first button to turn it on.", "",
"", "Read this document carefully as all the described concepts are vital for using the AUTOCAL unit.",
"## About MS-ES1", "",
"The script read by AUTOCAL units is written in *Machine Script - Equestrian Standard, Version 1*, or MS-ES1. MS-ES1 features named variables, value comparison, (conditional) jumping, evaluation of mathematical expressions and reading/writing of RoR signals. The speed at which lines are processed is defined by the *clock speed*, which can be defined in the script. The amount describes the number of lines processed per tick, i.e. the default clock speed of 1 means 20 lines are processed per second (there are 20 ticks in one second). The maximum clock speed is determined by the server config value `AUTOCAL_MAX_CLOCK` (default is 20, i.e. 400 per second).", "## About AUTOCAL",
"", "The AUTOCAL automatic calculator is a basic machine that reads a script, line by line, and performs actions based on those lines. This means it can be programmed with behavior, doing math, and handling signals. It communicated with the outside world exclusively using Redstone-over-Radio (RoR) signals, allowing RoR to supply it with values to be processed, and returning RoR signals to be displayed or for controlling RoR receiving devices.",
"Example: `/ntmserver set AUTOCAL_MAX_CLOCK 10` -> Sets the max clock speed to 10.", "",
"", "The first button is the on/off switch. Turning the AUTOCAL unit on will start the script at the first line. If the script concludes, an error is encountered (without the \"ignore errors\" setting enabled), the `shutdown` command is used or no program is loaded to begin with, the unit will automatically power down.",
"## About the Buffer", "",
"The buffer is a single \"slot\" of information that can be used for many commands. Some commands produce an output which is saved to the buffer, some commands modify the contents of the buffer, and some commands use the buffer's contents. The buffer's contents can also be saved permanently as a named variable, and named variables can be written back into the buffer again if needed. The buffer persists as long as the AUTOCAL unit is running, should it restart or shut down, the buffer's contents are lost.", "The second button is the \"ignore errors\" setting. The red X means the setting is disabled, if an error is encountered in the program (an unrecognized command, missing or incorrect parameters, anything the script isn't meant to handle) then the unit will shut off. Otherwise, the erroring instruction is simply skipped. This may cause undefined or unexpected behavior, however in certain circumstances this doesn't matter. It's advised to keep this turned off, as it makes it easier to find obvious errors in the script.",
"", "",
"## About Variables", "The third button is the \"automatic reboot\" setting. If the unit is powered down, no matter the reason, it will automatically try to start again. This allows for a simple \"loop\" where the same script is repeated every time it ends by simply rebooting the AUTOCAL unit at the end. Do note that doing so will delete all saved variables, more on how variables work later.",
"MS-ES1 allows named variables to be saved for later use. There is no limit to how many variables can be saved. All variables are text (\"Strings\"), however depending on the command and context, that text may be interpreted as a number (both full and decimal). Variables, much like the buffer, are also stored as long as the program is running, if the AUTOCAL unit shuts down, all stored variables are lost.", "",
"", "The fourth button is for uploading the script. The script is in the minecraft's install folder, under the `hbmComputerUpload` folder, named `script.txt`. Clicking this button will send the contents of this script file to the AUTOCAL unit.",
"## About Variable Substitution", "",
"Many commands allow for *variable substitution*, i.e. a specific format can be used to insert the contents of a variable (or multiple!) into a parameter. This allows for the quick use of variables, or multiple variables in the same statement. Substitution is defined by `$variable name$`, where this text is replaced with the value of a variable called \"variable name\".", "The fifth button is for opening the script file. If no script file exists yet, the folder and empty script will be created.",
"", "",
"Example: `eval $val1$ + $val2$` assuming `val1` is 4 and `val2` is 8 would resolve to `eval 4 + 8`.", "The sixth button is for downloading a script file. The existing `script.txt` is deleted and replaced with the script that is loaded onto the AUTOCAL unit.",
"", "",
"Special case: The contents of the buffer can also be accessed using substitution using `$buffer$`. Consequently, a variable also named \"buffer\" can **not** be accessed using variable substitution at all.", "The simple workflow of programming an AUTOCAL unit is therefore using button #5 to open the file, writing the program, using #4 to flash the program to the AUTOCAL unit, and then using the first button to turn it on.",
"", "",
"## About Redstone-over-Radio", "## About MS-ES1",
"RoR has a specific limitation: A signal cannot be sent on the same channel within the same game tick. Subsequent signals on the same channel in the same tick will overwrite the previous one, with the exception of numeric (whole number) signals which are added together (e.g. sending \"5\" and \"7\" in the same tick creates a signal \"12\"). Since the AUTOCAL unit's clock speed allows it to theoretically send multiple signals on the same channel on the same tick, it might be necessary to *end* the operation for this tick even though there's still clock cycles left to do. This can be done with the `endtick` command.", "The script read by AUTOCAL units is written in *Machine Script - Equestrian Standard, Version 1*, or MS-ES1. MS-ES1 features named variables, value comparison, (conditional) jumping, evaluation of mathematical expressions and reading/writing of RoR signals. The speed at which lines are processed is defined by the *clock speed*, which can be defined in the script. The amount describes the number of lines processed per tick, i.e. the default clock speed of 1 means 20 lines are processed per second (there are 20 ticks in one second). The maximum clock speed is determined by the server config value `AUTOCAL_MAX_CLOCK` (default is 20, i.e. 400 per second).",
"", "",
"## The Script (Commands)", "Example: `/ntmserver set AUTOCAL_MAX_CLOCK 10` -> Sets the max clock speed to 10.",
"", "",
"### Comments", "## About the Buffer",
"Lines that start with `# ` (hashtag + space) are comments, and therefore ignored. If the AUTOCAL encounters such a line, it is skipped, not using up the clock cycle.", "The buffer is a single \"slot\" of information that can be used for many commands. Some commands produce an output which is saved to the buffer, some commands modify the contents of the buffer, and some commands use the buffer's contents. The buffer's contents can also be saved permanently as a named variable, and named variables can be written back into the buffer again if needed. The buffer persists as long as the AUTOCAL unit is running, should it restart or shut down, the buffer's contents are lost.",
"", "",
"Example: `# This is a comment` -> A line that does nothing, but can still be useful to explain and annotate other commands.", "## About Variables",
"", "MS-ES1 allows named variables to be saved for later use. There is no limit to how many variables can be saved. All variables are text (\"Strings\"), however depending on the command and context, that text may be interpreted as a number (both full and decimal). Variables, much like the buffer, are also stored as long as the program is running, if the AUTOCAL unit shuts down, all stored variables are lost.",
"### nop", "",
"`nop` (no operation) is an operation that consumes one clock cycle, but does not have any other effect. This is only useful in special cases where clock cycle timing is somehow important.", "## About Variable Substitution",
"", "Many commands allow for *variable substitution*, i.e. a specific format can be used to insert the contents of a variable (or multiple!) into a parameter. This allows for the quick use of variables, or multiple variables in the same statement. Substitution is defined by `$variable name$`, where this text is replaced with the value of a variable called \"variable name\".",
"### clockspeed", "",
"`clockspeed <speed>` sets the AUTOCAL's clock speed (i.e. amount of lines processed per tick). This can be changed at any point in the script, but usually it is most useful to start the script by defining the clock speed.", "Example: `eval $val1$ + $val2$` assuming `val1` is 4 and `val2` is 8 would resolve to `eval 4 + 8`.",
"", "",
"Example: `clockspeed 5` -> Sets the AUTOCAL's clock speed to five lines per tick (100 lines per second).", "Special case: The contents of the buffer can also be accessed using substitution using `$buffer$`. Consequently, a variable also named \"buffer\" can **not** be accessed using variable substitution at all.",
"", "",
"### dest", "## About Redstone-over-Radio",
"`dest <name>` creates a jump destination. Using the various jumping conditions, we can cause the AUTOCAL to return back (or forward) to this point. If a destination is reached not by jumping but simply by reading the next line, it will not use up a clock cycle, just like a comment. Destinations need unique names, if multiple destinations exist with the same name, then the latter ones will overwrite the former ones.", "RoR has a specific limitation: A signal cannot be sent on the same channel within the same game tick. Subsequent signals on the same channel in the same tick will overwrite the previous one, with the exception of numeric (whole number) signals which are added together (e.g. sending \"5\" and \"7\" in the same tick creates a signal \"12\"). Since the AUTOCAL unit's clock speed allows it to theoretically send multiple signals on the same channel on the same tick, it might be necessary to *end* the operation for this tick even though there's still clock cycles left to do. This can be done with the `endtick` command.",
"", "",
"Example: `dest start` -> Creates a destination point named \"start\", any jump instruction using \"start\" will cause the script to return to this point.", "## The Script (Commands)",
"", "",
"### jmp", "### Comments",
"`jmp <destination>` will cause the program to skip to the destination with the supplied name. The jump will always be performed when the script reaches this instruction. Supports variable substitution!", "Lines that start with `# ` (hashtag + space) are comments, and therefore ignored. If the AUTOCAL encounters such a line, it is skipped, not using up the clock cycle.",
"", "",
"Example: `jmp start` -> Jumps to the destination point named \"start\".", "Example: `# This is a comment` -> A line that does nothing, but can still be useful to explain and annotate other commands.",
"Example: `jmp $dest$` -> Jumps to the destination point with the same name as the contents of the variable \"dest\".", "",
"", "### nop",
"### jmpif", "`nop` (no operation) is an operation that consumes one clock cycle, but does not have any other effect. This is only useful in special cases where clock cycle timing is somehow important.",
"`jmpif <destination>` will cause the program to skip to the destination with the supplied name, if the buffer's content is `true`. Otherwise, the program will just proceed to the next line, like any other instruction. Supports variable substitution!", "",
"", "### clockspeed",
"Example:", "`clockspeed <speed>` sets the AUTOCAL's clock speed (i.e. amount of lines processed per tick). This can be changed at any point in the script, but usually it is most useful to start the script by defining the clock speed.",
"`jmpif skip`", "",
"`nop`", "Example: `clockspeed 5` -> Sets the AUTOCAL's clock speed to five lines per tick (100 lines per second).",
"`dest skip`", "",
"`nop`", "### dest",
"", "`dest <name>` creates a jump destination. Using the various jumping conditions, we can cause the AUTOCAL to return back (or forward) to this point. If a destination is reached not by jumping but simply by reading the next line, it will not use up a clock cycle, just like a comment. Destinations need unique names, if multiple destinations exist with the same name, then the latter ones will overwrite the former ones.",
"-> If the buffer is `true`, then the program will jump to the destination point named \"skip\" and only run the second `nop`. Otherwise, both the first and second `nop` will run.", "",
"", "Example: `dest start` -> Creates a destination point named \"start\", any jump instruction using \"start\" will cause the script to return to this point.",
"### jmpnot", "",
"`jmpnot <destination>` will cause the program to skip to the destination with the supplied name, if the buffer's content is **not** `true`. This is basically the inverse of `jmpif`. Only exists for some convenience for people who are used to more traditional languages' `if` statements. Supports variable substitution!", "### jmp",
"", "`jmp <destination>` will cause the program to skip to the destination with the supplied name. The jump will always be performed when the script reaches this instruction. Supports variable substitution!",
"Example:", "",
"`jmpnot skip` <- if", "Example: `jmp start` -> Jumps to the destination point named \"start\".",
"`nop` <- then", "Example: `jmp $dest$` -> Jumps to the destination point with the same name as the contents of the variable \"dest\".",
"`jmp end`", "",
"`dest skip`", "### jmpif",
"`nop` <- else", "`jmpif <destination>` will cause the program to skip to the destination with the supplied name, if the buffer's content is `true`. Otherwise, the program will just proceed to the next line, like any other instruction. Supports variable substitution!",
"`dest end` <- end", "",
"", "Example:",
"-> By using two jumps, one conditional and one fixed, we can emulate the structure of an `if/else` block. If the buffer's content is `true`, the first `nop` runs. Otherwise, the second `nop` is run. The second jump (`jmp end`) lets us skip over the \"else\" part of the script after running the \"then\" part.", "`jmpif skip`",
"", "`nop`",
"### endtick", "`dest skip`",
"`endtick` stops the script until the next game tick. This is important when sending multiple RoR signals on the same channel in a row, since a channel in the same tick can only hold one signal.", "`nop`",
"", "",
"### shutdown", "-> If the buffer is `true`, then the program will jump to the destination point named \"skip\" and only run the second `nop`. Otherwise, both the first and second `nop` will run.",
"`shutdown` will turn the AUTOCAL unit off. If the AUTOCAL is set up to automatically reboot, this effectively restarts the script from scratch, voiding all saved variables and the buffer. If not, then the AUTOCAL unit will stay off until manually restarted.", "",
"", "### jmpnot",
"### load", "`jmpnot <destination>` will cause the program to skip to the destination with the supplied name, if the buffer's content is **not** `true`. This is basically the inverse of `jmpif`. Only exists for some convenience for people who are used to more traditional languages' `if` statements. Supports variable substitution!",
"`load <name>` will take the value of a variable with the supplied name and copy it to the buffer. Many commands work directly out of the buffer, so next to variable substitution, this is the only way of actually accessing the contents of a variable.", "",
"", "Example:",
"Example: `load val` -> Assuming that the variable \"val\" contains the value `5`, then the buffer's value is now overwritten with that `5`.", "`jmpnot skip` <- if",
"", "`nop` <- then",
"### save", "`jmp end`",
"`save <name>` will take the value of the buffer and save it to a variable with the supplied name. This is the only way of actually changing variables, and saving values for later use besides the buffer.", "`dest skip`",
"", "`nop` <- else",
"Example: `save val` -> Assuming that the buffer contains the value `12`, this will create a new variable named \"val\" with the value of `12`.", "`dest end` <- end",
"", "",
"### buffer", "-> By using two jumps, one conditional and one fixed, we can emulate the structure of an `if/else` block. If the buffer's content is `true`, the first `nop` runs. Otherwise, the second `nop` is run. The second jump (`jmp end`) lets us skip over the \"else\" part of the script after running the \"then\" part.",
"`buffer <value>` will write the supplied value directly to the buffer. This means that commands that require buffer values can be supplied with values directly from the code.", "",
"", "### endtick",
"Example:", "`endtick` stops the script until the next game tick. This is important when sending multiple RoR signals on the same channel in a row, since a channel in the same tick can only hold one signal.",
"`buffer Horseshoe`", "",
"`save item`", "### shutdown",
"", "`shutdown` will turn the AUTOCAL unit off. If the AUTOCAL is set up to automatically reboot, this effectively restarts the script from scratch, voiding all saved variables and the buffer. If not, then the AUTOCAL unit will stay off until manually restarted.",
"-> Will buffer the value `Horseshoe` and then save it to the variable called \"item\".", "",
"", "### load",
"### eval", "`load <name>` will take the value of a variable with the supplied name and copy it to the buffer. Many commands work directly out of the buffer, so next to variable substitution, this is the only way of actually accessing the contents of a variable.",
"`eval [statement]` will evaluate the supplied statement as a mathematical expression. In short, anything NTM's calculator can make sense of (the one you open with N by default), this function can do the same. The statement is optional, if no statement is supplied, then it will try to use the buffer's contents as the statement. `eval` produces **decimal** values and not whole number **integers**, so for use in RoR signals, the output needs to be rounded! The result of the calculation is then saved to the buffer. Supports variable substitution!", "",
"", "Example: `load val` -> Assuming that the variable \"val\" contains the value `5`, then the buffer's value is now overwritten with that `5`.",
"Example: `eval 4 + 5` -> Calculates \"4+5\" and saves `9` to the buffer.", "",
"Example: `eval $val$ / 2` -> Takes the value of \"val\" and divides it by 2, saving the result to the buffer.", "### save",
"", "`save <name>` will take the value of the buffer and save it to a variable with the supplied name. This is the only way of actually changing variables, and saving values for later use besides the buffer.",
"Example:", "",
"`load calc`", "Example: `save val` -> Assuming that the buffer contains the value `12`, this will create a new variable named \"val\" with the value of `12`.",
"`eval`", "",
"", "### buffer",
"-> Will write the value of \"calc\" to the buffer, and then treat it as a mathematical expression. If we assume \"calc\" to be `5+2` then `eval` will end up writing `7` to the buffer.", "`buffer <value>` will write the supplied value directly to the buffer. This means that commands that require buffer values can be supplied with values directly from the code.",
"", "",
"### evalr", "Example:",
"`evalr [statement]` is identical to `eval`, however it will round the result to the nearest whole number. Whole numbers are important for RoR, since gauges, numeric displays and logic receivers can only handle whole numbers, and not decimals.", "`buffer Horseshoe`",
"", "`save item`",
"### rounddown / floor", "",
"`rounddown` or `floor` will try to interpret the buffer's content as a decimal, and round it **down** to the next lower integer, writing the result to the buffer again.", "-> Will buffer the value `Horseshoe` and then save it to the variable called \"item\".",
"", "",
"Example: `rounddown` -> Assuming the buffer's value is `4.2`, the buffer's new value will be `4`.", "### eval",
"Example: `rounddown` -> Assuming the buffer's value is `4.6`, the buffer's new value will be `4`.", "`eval [statement]` will evaluate the supplied statement as a mathematical expression. In short, anything NTM's calculator can make sense of (the one you open with N by default), this function can do the same. The statement is optional, if no statement is supplied, then it will try to use the buffer's contents as the statement. `eval` produces **decimal** values and not whole number **integers**, so for use in RoR signals, the output needs to be rounded! The result of the calculation is then saved to the buffer. Supports variable substitution!",
"", "",
"### roundup / ceil", "Example: `eval 4 + 5` -> Calculates \"4+5\" and saves `9` to the buffer.",
"`roundup` or `ceil` will try to interpret the buffer's content as a decimal, and round it **up** to the next higher integer, writing the result to the buffer again.", "Example: `eval $val$ / 2` -> Takes the value of \"val\" and divides it by 2, saving the result to the buffer.",
"", "",
"Example: `roundup` -> Assuming the buffer's value is `4.2`, the buffer's new value will be `5`.", "Example:",
"Example: `roundup` -> Assuming the buffer's value is `4.6`, the buffer's new value will be `5`.", "`load calc`",
"", "`eval`",
"### round / nearest", "",
"`round` or `nearest` will try to interpret the buffer's content as a decimal, and round it to the **closest** integer, writing the result to the buffer again.", "-> Will write the value of \"calc\" to the buffer, and then treat it as a mathematical expression. If we assume \"calc\" to be `5+2` then `eval` will end up writing `7` to the buffer.",
"", "",
"Example: `round` -> Assuming the buffer's value is `4.2`, the buffer's new value will be `4`.", "### evalr",
"Example: `round` -> Assuming the buffer's value is `4.6`, the buffer's new value will be `5`.", "`evalr [statement]` is identical to `eval`, however it will round the result to the nearest whole number. Whole numbers are important for RoR, since gauges, numeric displays and logic receivers can only handle whole numbers, and not decimals.",
"", "",
"### concat", "### rounddown / floor",
"`concat <text>` works similarly to `buffer <text>`, however it accepts variable substitution. This means that the text of multiple variables can be combined.", "`rounddown` or `floor` will try to interpret the buffer's content as a decimal, and round it **down** to the next lower integer, writing the result to the buffer again.",
"", "",
"Example: `concat $first$ and $second$` -> Assuming the variable \"first\" to be `Cats` and \"second\" to be `dogs`, then the result saved to the buffer is `Cats and dogs`.", "Example: `rounddown` -> Assuming the buffer's value is `4.2`, the buffer's new value will be `4`.",
"", "Example: `rounddown` -> Assuming the buffer's value is `4.6`, the buffer's new value will be `4`.",
"### eq", "",
"`eq <value>` - equals, will try to compare the buffer to the supplied value. The buffer will be set to `true` if the values are equal and `false` otherwise. Supports variable substitution!", "### roundup / ceil",
"", "`roundup` or `ceil` will try to interpret the buffer's content as a decimal, and round it **up** to the next higher integer, writing the result to the buffer again.",
"Example: `eq Brick` -> If the buffer is `Brick`, then it is set to `true`, otherwise it becomes `false`.", "",
"Example: `eq $comp$` -> If the buffer's value is equal to the value of the variable \"comp\", then it is set to `true`, otherwise it becomes `false`.", "Example: `roundup` -> Assuming the buffer's value is `4.2`, the buffer's new value will be `5`.",
"", "Example: `roundup` -> Assuming the buffer's value is `4.6`, the buffer's new value will be `5`.",
"### gtb", "",
"`gtb <value>` - greater than buffer, will try to compare the buffer to the supplied value. The buffer will be set to `true` if the supplied numerical value is greater and `false` otherwise. Supports variable substitution!", "### round / nearest",
"", "`round` or `nearest` will try to interpret the buffer's content as a decimal, and round it to the **closest** integer, writing the result to the buffer again.",
"Example: `gtb 4` -> If the buffer is `3` or lower, then it is set to `true`, otherwise it becomes `false`.", "",
"Example: `gtb $comp$` -> If the buffer lower than the value of \"comp\", then it is set to `true`, otherwise it becomes `false`.", "Example: `round` -> Assuming the buffer's value is `4.2`, the buffer's new value will be `4`.",
"", "Example: `round` -> Assuming the buffer's value is `4.6`, the buffer's new value will be `5`.",
"### ltb", "",
"`ltb <value>` - less than buffer, will try to compare the buffer to the supplied value. The buffer will be set to `true` if the supplied numerical value is lower and `false` otherwise. Supports variable substitution!", "### concat",
"", "`concat <text>` works similarly to `buffer <text>`, however it accepts variable substitution. This means that the text of multiple variables can be combined.",
"Example: `ltb 4` -> If the buffer is `5` or higher, then it is set to `true`, otherwise it becomes `false`.", "",
"Example: `ltb $comp$` -> If the buffer higher than the value of \"comp\", then it is set to `true`, otherwise it becomes `false`.", "Example: `concat $first$ and $second$` -> Assuming the variable \"first\" to be `Cats` and \"second\" to be `dogs`, then the result saved to the buffer is `Cats and dogs`.",
"", "",
"### geb", "### eq",
"`geb <value>` - greater than or equal buffer.", "`eq <value>` - equals, will try to compare the buffer to the supplied value. The buffer will be set to `true` if the values are equal and `false` otherwise. Supports variable substitution!",
"", "",
"### leb", "Example: `eq Brick` -> If the buffer is `Brick`, then it is set to `true`, otherwise it becomes `false`.",
"`leb <value>` - less than or equal buffer.", "Example: `eq $comp$` -> If the buffer's value is equal to the value of the variable \"comp\", then it is set to `true`, otherwise it becomes `false`.",
"", "",
"### send", "### gtb",
"`send <channel>` will send a Redstone-over-Radio signal over the supplied channel, with the signal's value being the current buffer's value. Sending repeatedly over the same channel requires waiting for a full game tick, so using `endtick` after sending is advised. Supports variable substitution!", "`gtb <value>` - greater than buffer, will try to compare the buffer to the supplied value. The buffer will be set to `true` if the supplied numerical value is greater and `false` otherwise. Supports variable substitution!",
"", "",
"Example:", "Example: `gtb 4` -> If the buffer is `3` or lower, then it is set to `true`, otherwise it becomes `false`.",
"`buffer Hello!`", "Example: `gtb $comp$` -> If the buffer lower than the value of \"comp\", then it is set to `true`, otherwise it becomes `false`.",
"`send transmission`", "",
"", "### ltb",
"-> Will send the RoR signal `Hello!` on the channel \"transmission\".", "`ltb <value>` - less than buffer, will try to compare the buffer to the supplied value. The buffer will be set to `true` if the supplied numerical value is lower and `false` otherwise. Supports variable substitution!",
"", "",
"Example:", "Example: `ltb 4` -> If the buffer is `5` or higher, then it is set to `true`, otherwise it becomes `false`.",
"`buffer SOS`", "Example: `ltb $comp$` -> If the buffer higher than the value of \"comp\", then it is set to `true`, otherwise it becomes `false`.",
"`send $target$`", "",
"", "### geb",
"-> Will send the RoR signal \"SOS\" to the channel saved in the variable \"target\".", "`geb <value>` - greater than or equal buffer.",
"", "",
"### listen ", "### leb",
"`listen <channel>` will listen in on the supplied RoR channel and write the signal to the buffer. Will detect all signals, even expired ones, and one just ones sent in the previous tick, so picking up a signal doesn't mean it's new information. Supports variable substitution!", "`leb <value>` - less than or equal buffer.",
"", "",
"Example:", "### send",
"`listen input`", "`send <channel>` will send a Redstone-over-Radio signal over the supplied channel, with the signal's value being the current buffer's value. Sending repeatedly over the same channel requires waiting for a full game tick, so using `endtick` after sending is advised. Supports variable substitution!",
"`eval $buffer$ * 100`", "",
"`send output`", "Example:",
"", "`buffer Hello!`",
"-> Will take the signal from the RoR channel \"input\", multiply it by 100, and send that value on the channel \"output\".", "`send transmission`",
"", "",
"## Advanced", "-> Will send the RoR signal `Hello!` on the channel \"transmission\".",
"", "",
"### Conditional Branches", "Example:",
"Basic if/else conditions are the bread and butter of most programming. If we want to change the behavior based on different values, we need to compare, and then conditional jump.", "`buffer SOS`",
"", "`send $target$`",
"This example creates a script that processed a number \"val\" based on how high it is. 4 and below are multiplied by 2, otherwise it is divided by 2:", "",
"", "-> Will send the RoR signal \"SOS\" to the channel saved in the variable \"target\".",
"`buffer 4` <- buffer `4` for comparison", "",
"`gtb $val$` <- is \"val\" greater than our buffer?", "### listen ",
"`jmpnot else` <- if not, jump to \"else\"", "`listen <channel>` will listen in on the supplied RoR channel and write the signal to the buffer. Will detect all signals, even expired ones, and one just ones sent in the previous tick, so picking up a signal doesn't mean it's new information. Supports variable substitution!",
"`eval $val$ / 2` <- if it is, divide by 2", "",
"`save val` <- ...and save to \"val\"", "Example:",
"`jmp end` <- now jump to \"end\" to skip our \"else\" block", "`listen input`",
"`dest else` <- else...", "`eval $buffer$ * 100`",
"`eval $val$ * 2` <- multiply by 2", "`send output`",
"`save val` <- ...and save to val", "",
"`dest end` <- no matter which branch we took, we always end up here", "-> Will take the signal from the RoR channel \"input\", multiply it by 100, and send that value on the channel \"output\".",
"", "",
"### Methods", "## Advanced",
"People who are used to high languages will already know this concept, reusable parts of code with a set of parameters and an optional return value. For this we will use a few variables and a set of jumps in order to be able to use this piece of code from anywhere:", "",
"", "### Conditional Branches",
"This example implements a basic lerp (linear interpolation) function.", "Basic if/else conditions are the bread and butter of most programming. If we want to change the behavior based on different values, we need to compare, and then conditional jump.",
"", "",
"`dest lerp`", "This example creates a script that processed a number \"val\" based on how high it is. 4 and below are multiplied by 2, otherwise it is divided by 2:",
"`eval $a$ + ($b$ - $a$) * $i$`", "",
"`save result`", "`buffer 4` <- buffer `4` for comparison",
"`jmp $return$`", "`gtb $val$` <- is \"val\" greater than our buffer?",
"", "`jmpnot else` <- if not, jump to \"else\"",
"This function requires the variables \"a\", \"b\" and \"i\" as parameters and saves the result to the variable \"result\". We can now access this function like such:", "`eval $val$ / 2` <- if it is, divide by 2",
"", "`save val` <- ...and save to \"val\"",
"`buffer 4` <- first we set up our parameters to be used in the function", "`jmp end` <- now jump to \"end\" to skip our \"else\" block",
"`save a`", "`dest else` <- else...",
"`buffer 7`", "`eval $val$ * 2` <- multiply by 2",
"`save b`", "`save val` <- ...and save to val",
"`buffer 0.6`", "`dest end` <- no matter which branch we took, we always end up here",
"`save i`", "",
"`buffer returnhere` <- then we define the name of the return point", "### Methods",
"`save return`", "People who are used to high languages will already know this concept, reusable parts of code with a set of parameters and an optional return value. For this we will use a few variables and a set of jumps in order to be able to use this piece of code from anywhere:",
"`jmp lerp` <- call the function", "",
"`dest returnhere` <- once the function concludes, we are back here", "This example implements a basic lerp (linear interpolation) function.",
"", "",
"At the end of it all, the variable \"result\" now has the desired value.", "`dest lerp`",
}; "`eval $a$ + ($b$ - $a$) * $i$`",
} "`save result`",
"`jmp $return$`",
"",
"This function requires the variables \"a\", \"b\" and \"i\" as parameters and saves the result to the variable \"result\". We can now access this function like such:",
"",
"`buffer 4` <- first we set up our parameters to be used in the function",
"`save a`",
"`buffer 7`",
"`save b`",
"`buffer 0.6`",
"`save i`",
"`buffer returnhere` <- then we define the name of the return point",
"`save return`",
"`jmp lerp` <- call the function",
"`dest returnhere` <- once the function concludes, we are back here",
"",
"At the end of it all, the variable \"result\" now has the desired value.",
};
}