To see full list of supported features you can check test cases here:
src/test/java/com/zergatul/scripting/tests/compiler
Although this scripting language was designed for Minecraft mod, it doesn't have any dependencies on Minecraft. The only dependency it has is ASM library (for emitting Java bytecode). The language itself can be used for something else.
This scripting language allows mod users to create custom scripts, bind them to keys, automate tasks, react to in-game events by using C#/Java-like syntax. It is lightweight, async-friendly, and designed to interop with Java APIs where needed.
- Hello World
- Basic Types
- Variables
- Arrays
- Static Variables
- Control Flow
- Parsing Strings
- Functions
- Type Check/Cast
- null
- Reflection
- Classes
- Java Interop
- Limitations
- Comparison Table
debug.write("Hello World!");booleancharint8(only use it for Java interop)int16(only use it for Java interop)int(synonym:int32)long(synonym:int64)float32(corresponds tofloatin Java, only use it for Java interop)float(corresponds todoublein Java, synonym:float64)string
Unsigned integer types are not supported.
Language is not well adapted for using int8/int16/float32. If you can you should better use int and float instead.
int x; // will have value 0 assigned implicitly
float f = 11.25;
let s = "qq"; // variable "s" will have "string" typeint[] array1; // will have zero length array assigned implicitly
int[] array2 = new int[5];
let array3 = new int[] { 10, 20, 30, 40, 50 };
let array4 = [1, 2, 3];
let array5 = []; // not allowed, cannot infer type of array5
int[] array6 = []; // allowed
int[][] array7 = new int[][10]; // array of 10 arrays, by default with null values
// array concatenation
int[] a1 = [1, 2, 3];
int[] a2 = [7, 8, 9];
int[] a3 = a1 + a2 + 10; // a3=[1,2,3,7,8,9,10]Array length is static, it cannot be resized. "+" operation always creates new array.
Static variables should be defined in the beginning of the script, along with functions/classes. These variables preserve values across multiple script invocations (meaning when mod engine executes script in response to some event). Imagine below script bound to some key:
static int x1;
int x2;
x1++;
x2++;
debug.write(x1.toString()); // increases each time you press a key
debug.write(x2.toString()); // always logs 1if (api.getCount() > 10) {
debug.write("OK");
} else {
debug.write("NOT OK");
}int x = 123;
int y = x > 100 ? x - 100 : x + 100;let array = [1, 2, 3];
for (let i = 0; i < array.length; i++) {
debug.write(i.toString());
}foreach loop works only with arrays.
let array = [1, 2, 3];
foreach (let x in array) {
debug.write(x.toString());
}continue, break statements are supported. do/while loops are not supported.
int x;
if (int.tryParse("123", ref x)) {
// success
// x is 123 here
} else {
// fail
}Functions should be defined in the beginning of the script, before all script statements:
int factorial(int value) {
if (value <= 1) {
return 1;
} else {
return value * factorial(value - 1);
}
}
debug.write(factorial(10).toString());You can also use arrow syntax if function is short:
int sum(int a, int b) => a + b;Function overloading is not supported:
void func() {}
void func(int x) {} // will not compileSubscribing to event:
events.onTickEnd(() => {
debug.write("Tick: #" + game.getTick());
});Using lambda for filtering:
inventory.findAndMoveToHotbar(1, (stack) => {
return stack.item.id == "minecraft:wooden_sword";
});
// or
inventory.findAndMoveToHotbar(1, stack => stack.item.id == "minecraft:wooden_sword");Lambda functions with explicit types are not supported:
// not supported
inventory.findAndMoveToHotbar(1, (ItemStack stack) => stack.item.id == "minecraft:wooden_sword");boolean filterSword(ItemStack stack) {
return stack.item.id == "minecraft:wooden_sword";
}
inventory.findAndMoveToHotbar(1, filterSword);Class methods can be used as functions:
class MyClass {
void method(int x) {
debug.write((x * x).toString());
}
}
void test(fn<int => void> func) {
func(10);
}
let my = new MyClass();
test(my.method); // prints "100", captures "my" variable into closureYou can describe functional types like this: fn<() => void> / fn<int => string> / fn<(int, int, int) => fn<int => int>>
You can use them as function parameters:
void run(fn<() => void> func, int times) {
for (let i = 0; i < times; i++) {
func();
}
}
run(() => debug.write("a"), 3); // writes "a" to debug 3 timesAs local variables:
int x = 3;
fn<int => int> add3 = a => a + x;
debug.write(add3(5).toString()); // writes 8Functions can be cast to functional type:
void write(string value) {
debug.write(value);
}
fn<string => void> f = write;
f();In async context you can use await statements:
for (let i = 0; i < 10; i++) {
await delay.ticks(1);
ui.systemMessage("Iteration " + i);
}You can declare your own async functions:
async void loop() {
for (let i = 0; i < 5; i++) {
ui.systemMessage(game.getTick().toString());
await delay.ticks(10);
}
}
if (api.getSomething()) {
await loop();
} else {
ui.systemMessage("no");
}You can call async function without await. In this case function will run in "background":
async void loop() {
for (let i = 0; i < 5; i++) {
ui.systemMessage(game.getTick().toString());
await delay.ticks(10);
}
}
loop();
loop();
// 2 loops will run at the same timeAsync functions can also return any type:
async int waitForChestAndCountItems(string itemId) {
while (containers.getMenuClass() != "net.minecraft.world.inventory.ChestMenu") {
await delay.ticks(1);
}
int count = 0;
int slots = containers.getSlotsSize();
for (let i = 0; i < slots; i++) {
let stack = containers.getItemAtSlot(i);
if (stack.item.id == itemId) {
count += stack.count;
}
}
return count;
}let x = api.getSomething();
if (x is ItemStack) {
let stack = x as ItemStack;
debug.write(stack.item.name);
}This works for basic types like int, string, for defined classes, and for Java interop types, like Java<java.lang.Object>.
There is no null keyword in the language. If you have to check if some value is null, you can use is expression:
let possiblyNull = getSomething();
if (possiblyNull is ExpectedType) {
// possiblyNull != null
} else {
// possiblyNull == null
}Normally APIs to be used from scripting language should not return or expect null. There is no simple way to assign/pass null value.
let x = "123";
let type = #typeof(x); // type is instance of Type
debug.write(type.name); // logs "string"
if (type == #type(string)) {
// compare 2 types
}Classes should be defined in the beginning of the script, before all script statements. Class can have fields, constructors, methods. Class without constructors receive implicit parameterless constructor:
class MyClass {
int x;
int y;
}
let c = new MyClass();
c.x = 1;
c.y = 2;
debug.write((c.x + c.y).toString());For constructor/method bodies you can use square brackets, or arrow if method is short:
class MyClass {
int x;
constructor(int x) {
this.x = x;
}
// or
// constructor(int x) => this.x = x;
int getX() {
return x;
}
// or
// int getX() => x;
void inc() {
x++;
}
// or
// void inc() => x++;
}Async methods supported.
Limitations:
- access modifiers (private/public/etc) are not supported
- static members are not supported
- inheritance is not supported
- generics not supported
Generic type syntax is not supported.
let table = new Java<java.util.Hashtable>();
table.put(false, 100);
table.put(200, true);
table.put("qq", "ww");
debug.write(table.get("qq").toString()); // ww
debug.write(table.get(200).toString()); // true
debug.write(table.get(false).toString()); // 100
// if you need to cast Object to specific type
let obj = table.get("qq"); // type: Java<java.lang.Object>
let str = obj as string; // cast to string
debug.write(str);try/catchnot supported- operator overloading not supported
- Java interop with parameterized types (generics) is not supported
| C# | Scripting Language |
|---|---|
var |
let |
(int)x |
x as int |
x as int |
Not supported (only cast syntax exists) |
x is ClassA |
x is ClassA |