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
- Expressions
- Arrays
- Static Variables
- Control Flow
- Parsing Strings
- Functions
- Type Check/Cast
isoperator and pattern matching- null
- Boxing
- Reflection
- Classes
- Extensions
- Java Interop
- Type Aliases
- Exceptions
- 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.
The built-in string type is directly backed by java.lang.String on the JVM.
- At runtime, a value of type
stringis a normal Javajava.lang.Stringinstance. - The language adds its own conveniences on top of it:
- Indexer:
s[i]instead ofs.charAt(i) - Read-only length property:
s.length
- Indexer:
- Because of this, you should treat
stringas the canonical text type in this language.
For low-level interop you can still expose the raw Java type, e.g.:
typealiasJString=Java<java.lang.String>;Both string and Java<java.lang.String> share the same underlying Java class. All type checks and casts (is / as) are based on that underlying Java class, so they are compatible:
Java<java.lang.Object>obj= ...;if(objisstring){// true if obj actually contains a java.lang.String}if(objisJString){// also true in the same case}strings1=objasstring;JStrings2=objasJString;Use string for normal scripting and only use Java<java.lang.String> (or its alias) when you specifically need to call Java APIs that require an explicit java.lang.String type, or you are doing low-level reflection.
intx;// will have value 0 assigned implicitlyfloatf=11.25;lets="qq";// variable "s" will have "string" typeSupported binary operators: +, -, *, /, %, &&, ||, ==, !=, <, >, <=, >=, &, |, is, as, in
Supported unary operators: +, -, !
in operator is just a syntactic sugar for contains method that returns boolean and accepts single argument. You can also declare extension contains method and in operator will work for this:
extension(int){booleancontains(intvalue)=>value.toString()inthis.toString();}
boolean b1=12in123;// true, equivalent: "123".contains("12")booleanb2=34in123;// false, equivalent: "123".contains("34")int[]array1;// will have zero length array assigned implicitlyint[]array2=newint[5];letarray3=newint[]{10,20,30,40,50};letarray4=[1,2,3];letarray5=[];// not allowed, cannot infer type of array5int[]array6=[];// allowedint[][]array7=newint[][10];// array of 10 arrays, by default with null values// array concatenationint[]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:
staticintx1;intx2;x1++;x2++;debug.write(x1.toString());// increases each time you press a keydebug.write(x2.toString());// always logs 1if(api.getCount()>10){debug.write("OK");}else{debug.write("NOT OK");}intx=123;inty=x>100?x-100:x+100;letarray=[1,2,3];for(leti=0;i<array.length;i++){debug.write(i.toString());}foreach loop works only with arrays.
letarray=[1,2,3];foreach(letxinarray){debug.write(x.toString());}continue, break statements are supported. do/while loops are not supported.
intx;if(int.tryParse("123",refx)){// success// x is 123 here}else{// fail}Functions should be defined in the beginning of the script, before all script statements:
intfactorial(intvalue){if(value<=1){return1;}else{returnvalue*factorial(value-1);}}debug.write(factorial(10).toString());You can also use arrow syntax if function is short:
intsum(inta,intb)=>a+b;Function overloading is supported:
intmax(inti1,inti2)=>i1>i2?i1:i2;intmax(inti1,inti2,inti3)=>max(i1,max(i2,i3));intmax(inti1,inti2,inti3,inti4)=>max(max(i1,i2),max(i3,i4));Subscribing to event:
events.onTickEnd(()=>{debug.write("Tick: #"+game.getTick());});Using lambda for filtering:
inventory.findAndMoveToHotbar(1,(stack)=>{returnstack.item.id=="minecraft:wooden_sword";});// orinventory.findAndMoveToHotbar(1, stack =>stack.item.id=="minecraft:wooden_sword");Lambda functions with explicit types are not supported:
// not supportedinventory.findAndMoveToHotbar(1,(ItemStackstack)=>stack.item.id=="minecraft:wooden_sword");booleanfilterSword(ItemStackstack){returnstack.item.id=="minecraft:wooden_sword";}inventory.findAndMoveToHotbar(1,filterSword);Class methods can be used as functions:
classMyClass{voidmethod(intx){debug.write((x*x).toString());}}voidtest(fn<int=>void>func){func(10);}letmy=newMyClass();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:
voidrun(fn<()=>void>func,inttimes){for(leti=0; i <times; i++){func();}}run(()=>debug.write("a"),3);// writes "a" to debug 3 timesAs local variables:
intx=3;fn<int=>int>add3= a =>a+x;debug.write(add3(5).toString());// writes 8Functions can be cast to functional type:
voidwrite(stringvalue){debug.write(value);}fn<string=>void>f=write;f();In async context you can use await statements:
for(leti=0;i<10;i++){awaitdelay.ticks(1);ui.systemMessage("Iteration "+i);}You can declare your own async functions:
asyncvoidloop(){for(leti=0;i<5;i++){ui.systemMessage(game.getTick().toString());awaitdelay.ticks(10);}}if(api.getSomething()){awaitloop();}else{ui.systemMessage("no");}You can call async function without await. In this case function will run in "background":
asyncvoidloop(){for(leti=0;i<5;i++){ui.systemMessage(game.getTick().toString());awaitdelay.ticks(10);}}loop();loop();// 2 loops will run at the same timeAsync functions can also return any type:
asyncintwaitForChestAndCountItems(stringitemId){while(containers.getMenuClass()!="net.minecraft.world.inventory.ChestMenu"){awaitdelay.ticks(1);}intcount=0;intslots=containers.getSlotsSize();for(leti=0;i<slots;i++){letstack=containers.getItemAtSlot(i);if(stack.item.id==itemId){count+=stack.count;}}returncount;}letx=api.getSomething();if(xisItemStackstack){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>.
as-expression features:
- if expression result can't be cast to target type, it evaluates as default value:
letx=1asfloat;// 1 is int, it can't be cast to float, x is set 0.0lety=newJava<java.lang.Object>()asstring;// y is set to null
- transparently handles value types and their boxed variants:
typealiasObject=Java<java.lang.Object>;typealiasInteger=Java<java.lang.Integer>;ObjectgetInt()=>10;leta=getInt()asint;// a is 10, unboxedletb=20asInteger;// b is 20, boxed
Use #cast(<expr>, <type>) expression for strong check cast. If expression can't be cast to type, ClassCastException is thrown.
typealiasObject=Java<java.lang.Object>;ObjectgetInt()=>10;letx= #cast(getInt(),int);// x is 10is operator supports basic pattern matching (similar to C#):
typealiasObject=Java<java.lang.Object>;// ...Objecto=func();// constant patternsif(oisnull){/* ... */}if(ois not null){/* ... */}if(ois100){/* ... */}if(ois not 200){/* ... */}if(ois"hello"){/* ... */}if(ois not "world"){/* ... */}// type patternsif(oisint){/* ... */}if(oisstring){/* ... */}// declaration patternif(oisstringstr){// str is defined here}else{// but not here}if(oisinti){// i is not defined here}else{// i can be used here}if(ois not floatf){return;}// f can be used hereMultiple declaration patterns are support in single condition:
Objecto1=func1();Objecto2=func1();if(o1isstringstr&&o2isintx){// str and x can be used here}However, pattern variables can't be used in the same expression:
Objecto1=func1();// will not work, str is not defined hereif(o1isstringstr&&str.length>5){}It is advised that APIs to be used from scripting language should not return or expect null.
This way it should be more beginner-friendly to not get NullReferenceException.
But for Java interop you most likely have to work with null a lot.
strings=null;if(s==null){debug.write("s is null");}Null-coalescing operator is supported:
strings1=null;strings2="hello";strings3=s1??s2;// s3 is "hello" herestrings4=s1??thrownewJava<java.lang.RuntimeException>();Wrapper classes (Java terminology) or boxed classes (C# terminology) are described like this: Boxed<int> (however you can't use such syntax in the code, you have to use Java<java.lang.Integer> instead).
Normally you don't need to use them explicitly, but you may often see them as parameters or return types when working with Java interop. Language supports automatic boxing/unboxing.
For example, API method may return Future<Boxed<boolean>>, because Future type corresponds to CompletableFuture under the hood, and it actually has java.lang.Boolean as type parameter. But you can write code like this:
booleanresult=awaitgame.connect("my.server.example.com");letx="123";lettype= #typeof(x);// type is instance of Typedebug.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, operator overloads. Class without constructors receives implicit parameterless constructor:
classMyClass{intx;inty;}letc=newMyClass();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:
classMyClass{intx;constructor(intx){this.x=x;}// or// constructor(int x) => this.x = x;intgetX(){returnx;}// or// int getX() => x;voidinc(){x++;}// or// void inc() => x++;}Fields, constructors, and methods can use public, protected, or private visibility. Omitting the modifier remains equivalent to public:
classMyClass{privateintvalue;protectedconstructor(intvalue)=>this.value=value;publicintgetValue()=>value;}Async methods supported.
Inheritance supported:
classBaseClass{virtualintcalc()=>12;}classChildClass:BaseClass{overrideintcalc()=>base.calc()+3;}Using virtual/override is required for polymorphism. Methods/fields shadowing is not allowed.
You can also extend Java classes:
classMyList:Java<java.util.Vector>{voidadd(intvalue)=>base.add(value);intget2(intindex)=>base.get(index)asint;}classFakeList:Java<java.util.Vector>{overrideintsize()=>0;}Constructor initializers:
classBaseClass{constructor(intvalue){}}classChildClass:BaseClass{constructor(inta,intb):base(a+b){}constructor(inta,intb,intc):this(a,b+c){}}Operator overloads:
classVec2{floatx;floaty;constructor(floatx,floaty){this.x=x;this.y=y;}operator[+]Vec2(Vec2vec)=>vec;operator[+]Vec2(Vec2left,Vec2right){returnnewVec2(left.x+right.x,left.y+right.y);}operator[-]Vec2(Vec2vec)=>newVec2(-vec.x,-vec.y,-vec.z);operator[-]Vec2(Vec2left,Vec2right){returnnewVec2(left.x-right.x,left.y-right.y);}operator[==]boolean(Vec2left,Vec2right)=>left.x==right.x&&left.y==right.y;operator[!=]boolean(Vec2left,Vec2right)=>left.x!=right.x||left.y!=right.y;}letv1=newVec2(1,2);letv2=newVec2(3,4);letv3=v1+v2;if(v2==newVec2(0,0)){/* ... */}Limitations:
- access modifiers (private/public/etc.) are not supported
- abstract classes not supported
- static members are not supported
- generics not supported
Extensions should be defined in the beginning of the script, before all script statements.
Extension blocks can have methods inside:
extension(int){intnext()=>this+1;intmore(intx)=>this+x;}inta=(10).next();// 11intb=a.more(5);// 16extension(int[]){booleancontains(intvalue){for(inti=0; i <this.length; i++){if(this[i]==value){return true;}}return false;}}
boolean b1 =1in[1,2,3];// truebooleanb2=4in[1,2,3];// falseExtension blocks can have operator overloads inside:
extension(string){operator[+]int(stringstr){intvalue;if(int.tryParse(str,refvalue)){returnvalue;}else{returnint.MIN_VALUE;}}}string s ="100"
int x =+s;// 100extension(string){operator[/]string[](stringstr,charch)=>str.split(ch);}letstr="hello world";let parts = str /' ';// ["hello", "world"]Generic type syntax is not supported.
lettable=newJava<java.util.Hashtable>();table.put(false,100);table.put(200,true);table.put("qq","ww");debug.write(table.get("qq").toString());// wwdebug.write(table.get(200).toString());// truedebug.write(table.get(false).toString());// 100// if you need to cast Object to specific typeletobj=table.get("qq");// type: Java<java.lang.Object>letstr=objasstring;// cast to stringdebug.write(str);To write complex scripts you often have to access private members. Language supports unique syntax to simplify this process:
typealiasMinecraft=Java<net.minecraft.client.Minecraft>;letmc=Minecraft.#instance;// accessing package-private static fieldmc.#rightClickDelay =0;// modifying private instance fieldmc.#handleKeybinds();// calling private methodAny "private" modifier is considered private by this syntax: private, package-private, protected.
This syntax doesn't switch the language into dynamic type mode (like dynamic keyword in C#),
every private access/call is validated during compilation.
Auto-completion is supported: obj.#<cursor> should pull private-only members.
Accessing private members is only allowed on Java<...> types.
Under the hood such calls are highly optimized by using MethodHandle objects.
Above example compiles to something like this in Java code:
classMethodHandleCache {
publicstaticfinalVarHandleinstance_var_handle;
publicstaticfinalVarHandlerightClickDelay_var_handle;
publicstaticfinalMethodHandlehandleKeybinding_method_handle;
staticMethodHandleCache() {
varcaller = MethodHandles.lookup();
varmcPrivateLookup = MethodHandles.privateLookupIn(Minecraft.class, caller);
instance_var_handle = mcPrivateLookup.findStaticVarHandle(Minecraft.class, "instance", Minecraft.class);
rightClickDelay_var_handle = mcPrivateLookup.findVarHandle(Minecraft.class, "rightClickDelay", int.class);
handleKeybinding_method_handle = mcPrivateLookup.findVirtual(Minecraft.class, "handleKeybinds", MethodType.methodType(void.class));
}
}
classScript {
publicvoidrun() {
varmc = (Minecraft) MethodHandleCache.instance_var_handle.get();
MethodHandleCache.rightClickDelay_var_handle.set(mc, 0);
MethodHandleCache.handleKeybinding_method_handle.invokeExact(mc);
}
}Type aliases should be defined in the beginning of the script, before all script statements. Example:
typealiasstr=string;voidlog(strs)=>debug.write(s);strx1="s1";log(x1);stringx2="s2";log(x2);The most useful case for it is Java interop:
typealiasMinecraft=Java<net.minecraft.client.Minecraft>;typealiasLocalPlayer=Java<net.minecraft.client.player.LocalPlayer>;typealiasClientLevel=Java<net.minecraft.client.multiplayer.ClientLevel>;LocalPlayerplayer=Minecraft.instance.player;ClientLevellevel=Minecraft.instance.level;3 variants supported:
try{ ...}catch{ ...}try{ ...}finally{ ...}try{ ...}catch{ ...}finally{ ...}catch-block may have optional variable where it will store Throwable instance:
try{(newint[0])[1]=0;}catch(e){// e is IndexOutOfRangeException}Only single catch-block supported. You can't filter exceptions by their types, like in Java or C#.
You can rethrow exception from catch-block:
try{// ...}catch(e){// ...throw;}To throw exception use throw statement:
typealiasRuntimeException=Java<java.lang.RuntimeException>;thrownewRuntimeException();In few cases throw behaves like expression, and not like statement:
// in lambdatypealiasRuntimeException=Java<java.lang.RuntimeException>;voidlog(fn<()=>int> func)=>debug.log(func().toString());log(()=>thrownewRuntimeException());// in conditional expressiontypealiasRuntimeException=Java<java.lang.RuntimeException>;booleanb=api.getSomething();intvalue=b?100:thrownewRuntimeException();// in null-coalescing expressiontypealiasRuntimeException=Java<java.lang.RuntimeException>;stringvalue=api.getStr()??thrownewRuntimeException();value??=thrownewRuntimeException();- Java interop with parameterized types (generics) is not supported
| C# | Scripting Language |
|---|---|
var | let |
(int)x | #cast(x, int) |
x as int | x as int |
x is ClassA | x is ClassA |