Other languages#
A place is Luau by default, but the engine under it does not know Luau. Every global, instance, signal and hook is described once, and that description is handed to whichever language runs the place. A language comes from an addon, and Moud ships two as examples: Revo and Java.
Picking the language#
You do not tell Moud which language you are using. It looks for the entry file with each known extension and runs the place in the language that has it:
place/
server/main.rv a Revo place
client/main.rvplace.tomlentries can leave the extension off (server = "res://server/Main"), and so canrequire. That way the same entry works whatever language the place ends up in.- Mixin files follow the place: a Revo place uses
client/mixins/*.rv.
A place with a main in two languages is an error, because Moud cannot choose between them. If you
are porting a place, move the old entry file out of the folder rather than leaving both in place.
Revo#
Revo looks like Luau with const and fn, and it calls the same API:
const world = game.world
const floor = world:add("Part", {name = "floor", size = vec3(20, 1, 20), position = vec3(0, 64, 0)})
game.stepped:connect(fn(dt) do
floor.transparency = 0.5 + 0.5 * math.sin(clock())
end)
task.spawn(fn() do
task.wait(2)
print("two seconds in")
end)- The names are the same as in Luau:
game,task,vec3,signal,java,mixin,shadersand the rest. What you learn on one page of these docs is what you write in Revo. - A place is type checked against the Moud api before it runs, so a wrong argument or an unknown
field stops it with the line. Editors read
.moud/moud.d.rv, written when the place starts in dev. task.waitworks insidetask.spawn,task.delayor aScriptinstance, the same as in Luau. On the main chunk it says so instead of waiting.task.spawnandtask.delaytake the function only, with no extra arguments after it.task.waitis the only call that waits. Anything else that waits in Luau, such assignal:wait(),httpor invoking aRemoteFunction, is an error in Revo.task.desynchronize,connectParallelandbindToMessageParalleldo not work in Revo, so anActorruns its Revo scripts serially.- The addon embeds Revo as a native library, built with Zig from a Revo checkout next to Moud.
Java#
A Java place is one public class per file, extending PlaceScript:
import com.meekdev.moud.addon.java.PlaceScript;
import com.meekdev.moud.core.instance.Instance;
import moud.Api;
public class Main extends PlaceScript {
@Override
public void run() {
Instance floor = add(world(), "Part", props("name", "floor", "size", vec3(20, 1, 20)));
onStep(dt -> set(floor, "transparency", 0.5 + 0.5 * Math.sin(Api.clock())));
spawn(() -> {
sleep(2);
print("two seconds in");
});
}
}run()is the entry point, and it does what the top of a Luau file does.PlaceScripthas the everyday helpers:world(),script(),add,get,set,call,tween,connect,onStep,onRenderStep,spawn,delay,every,sleep,props,vec3,color,euler,at,print. Properties go throughgetandsetbecause Java has no property syntax, andpropsbuilds the tableaddtakes.moud.Apiis the whole api with Java types, generated from the same description as the Luau types:Api.signal("hit"),Api.cooldown().ready(body, "dash", 1),Api.Methods.Instance.addTag(part, "hot").- Waiting is
sleep(seconds), becausewaitbelongs toObject. Like everywhere, it works insidespawn,delayor aScript. game.persistis a realMap.
Each file is compiled when the place loads, so a Java place needs a JDK, not only a JRE.
moud.Api is written to .moud/java/moud/Api.java, with the classpath in
.moud/java/classpath.txt, so an IDE completes it the way your editor completes
.moud/types.d.luau in a Luau place.
Writing a language addon#
An addon is a Fabric mod with a moud:addon entrypoint, see Writing an addon. A
language addon also implements LanguageAddon:
public final class MyAddon implements Addon, LanguageAddon {
public String id() { return "moud-mylang"; }
public ScriptLanguage language() { return new MyLanguage(); }
}"entrypoints": { "moud:addon": ["com.example.MyAddon"] },
"depends": { "moud": "*" }ScriptLanguage says what the language is called, which extensions it owns, how to start an engine
for a Host, and optionally how to write editor types:
public interface ScriptLanguage {
String name();
List<String> extensions();
ScriptEngine start(Host host);
default void writeTypes(Path place, Api api, ClassRegistry classes) throws IOException {}
}The extensions you return here are the ones Moud looks for when it picks the language for a place.
writeTypes has a default that does nothing, so you can leave editor types until later.
The engine runs chunks and modules, turns host functions into callable values and back, and gives the
host fibers so wait can park:
public interface ScriptEngine extends AutoCloseable {
void run(String chunk, String source);
Object module(String chunk, String source);
Fiber fiber(Callable fn);
Fiber script(String chunk, String source, Instance script);
ScriptValue table(Map<String, Object> data);
Map<String, Object> read(ScriptValue table);
void close();
}What the engine gets from the host, all language neutral:
host.globals(): every global value.Builtinis a function,HostObjectan object withgetandset,Invocablean object that can be called,Instance,Vector3,CFrameand the other values cross as themselves.host.index,host.assign,host.invokeandhost.operatedo the work for any of them.host.extensions(): members to add onto a global the language already has (Luau'smath).host.api(): the typed description of all of it, the input forwriteTypes.host.readScript(path): resolves arequirepath with the language's own extensions.- A host function that waits returns a
Suspend. A language with coroutines parks the fiber. One without can useThreadFiber, which is how the Java addon does it. host.onStepandhost.onRenderStephand the engine a tick when it needs to pump its own scheduler.
The Revo addon (addons/revo) is the example to copy for a language with its own VM, and the Java
addon (addons/java) for one that compiles to the JVM. Each has an example place in example/place,
which you run from the repository like any other place:
./gradlew :mod:runClient -Pplace=addons/revo/example/placeAddons in an exported game#
File > Export puts an addon in the game only when the place uses it:
- its language runs one of the entry scripts, like
server/main.rvfor Revo; - a scene holds an instance of a class it registers in
classes(ClassRegistry), or a script callsadd("ThatClass"); - or
place.tomlhas a table for it,[addons.<id>], where<id>is the addon'sid()or its Fabric mod id. See place.toml.
The export dialog lists each addon it includes and why, and the ones it leaves out. A place that needs an addon that is not installed can not be exported until it is. The addon goes in the game jar whole, with its own nested jars and native libraries and the mods it depends on, so players install nothing else.
