Moud

Places, modules, scenes and scripts#

A place is the game you write: a folder of files that Moud reads when the game starts and reads again every time you save. This page covers the file at its root that describes it, the folders inside it, how one file loads another, the scene files the editor writes, how you ship the result, how a server runs it, and the script instances you can put in the tree.

place.toml#

place.toml sits at the root of the place folder and says what the place is called, which files to run, and which parts of vanilla Minecraft are switched on. A place without one still runs, from server/main.luau and client/main.luau.

toml
name = "Spinning Floor"
id = "spinning-floor"
version = "0.1.0"
maxPlayers = 8

[entry]
server = "res://server/main.luau"
client = "res://client/main.luau"
loading = "res://client/loading.luau"   # the loading screen, before the server or the world
scene = "res://scenes/arena.scene"     # loaded under game.world before the server script runs

[features]
hunger = false
fallDamage = false
blockBreaking = false
chat = true
  • name is what the place is called, id and version name the file File > Export builds, and maxPlayers is how many players the place takes.
  • [entry] names the files Moud runs. server runs once on the server, client runs once inside each player's own game, and scene is loaded under game.world before the server script runs, so the server script finds the scene's instances already there. Without scene, scenes/main.scene is loaded when it exists, so an empty place.toml still starts in the scene a new place comes with. loading runs in the player's game as soon as it starts, and draws the loading screen until the place is running; it is optional, and client/loading.luau is used when it exists. See The loading screen. The same fields are under Entry in Project Settings, which refuses a loading script that does not exist.
  • [features] is a list of switches, one per vanilla Minecraft feature.
  • [server] sends players of the exported game straight to your server instead of a world of their own. See Sending players to your server.
  • [game] says which game the place belongs to and lists the game's other places. See Servers and games.
  • [page] holds the description, genre and pictures of the game's page on Moud. See Publishing a game.
  • [loading] describes the loading screen, see The loading screen, and says which files players of the published game download before they join. See Files players get first.
  • [addons.<id>] turns that addon on for the place and says File > Export puts it in the game. The table may be empty; its keys are the addon's own settings.

An unknown setting is an error that lists the ones that exist.

[features] turns parts of vanilla Minecraft on or off. The names are camel case:

Area Features
World sky, clouds, fog, stars, weather, dayNightCycle, ambientLight, terrain, blockBreaking, blockPlacing, blockEntities, fluids, mobs, itemDrops
Player hunger, healthRegen, fallDamage, drowning, fireDamage, vanillaMovement, inventory, crafting, xp, deathScreen
Screen hotbar, healthBar, hungerBar, xpBar, crosshair, effectIcons, bossBar, chat, tabList, scoreboard, hand, pauseMenu, debugScreen, titleScreen
Rendering and sound entityRendering, blobShadows, playerModel, nameTags, vanillaSounds, vanillaMusic
Scripts httpRequests

hand is off by default. The first person arm still shows while it is off, because a body shows it by default with appearance.firstPerson. Turning hand on shows the arm always, even for a body set to "none" or a player with no body. Only "body" hides it then. See What you see of your own body.

httpRequests is not a Minecraft feature: it lets server scripts call out to websites. See Reaching websites.

What [features] says is what every client starts with. A client script changes seven of the screen ones for itself while the place runs, with ui.setCoreGuiEnabled, and a reload puts them back the way this table asked for them. See the core interface.

An empty level#

Many places have no blocks at all: a game built from parts on a void, or a menu scene. Minecraft would still build, send and tick the empty chunks around each player, so Moud skips that work while there is nothing to do it for.

A place counts as empty while it plays with no terrain (no .polar for the start scene, or terrain = false), no generator, and no block placed yet. Then:

  • the server keeps the simulation distance at 2 chunks, and with a single player the view distance as well,
  • Minecraft's ambient particles, the ones blocks and biomes give off, stop while no block is within 32 blocks of you.

You do not see the difference: fog and sky still reach as far as the render distance you set. The first block anything places brings the full distances back, and so do a second player and the editor. A dedicated server only lowers the simulation distance, because its players draw their fog from its view distance.

Streaming#

Streaming sends each client only the instances near it, so a large world does not have to fit in every player's memory at once. It is on by default:

toml
[streaming]
enabled = true    # default
radius = 256      # metres, default 256, between 32 and 4096
  • The radius is measured from the player to each branch hanging off the root of the world. A branch further away than that is not sent, and is destroyed on that client until they come back. What is inside a branch goes with it, so a Model is sent or not sent whole.
  • alwaysRelevant on a Spatial sends it whatever the distance. It is read on the branch itself, the one hanging off the root, not on something buried inside one. Use it for a skybox, a boss, or anything a far-away script looks at.
  • Anything that is not a Spatial has no position, so it is always sent: values, remotes, containers, the leaderboard.
  • A property that points at an instance, like a weld's parts or an ObjectValue, is nil on a client while what it points at is too far away to be sent. It fills in when that instance arrives, and goes back to nil when it leaves.
  • enabled = false sends everything to everyone. Do that for a small place where a client script reads the whole world.
  • While you edit or playtest in the editor everything is sent, whatever this table says, so what the Explorer shows is the whole tree.

A branch that comes back is sent again from scratch, so a client script that kept a reference to something far away is holding a destroyed instance. Look things up when you need them.

Files players get first#

A published game does not make its players wait for every file. They download what the game needs to start, join, and get the rest while they play, as described in Publishing a game. Moud decides what comes first on its own: scripts, scenes and data, small files, and the pictures your start scene and loading screen name. Sounds, videos and big pictures come after.

When the game needs more than that from the first frame, list it under preload:

toml
[loading]
preload = ["res://images/ui/**", "res://music/menu.ogg"]

Each entry is a res:// path, and it may hold wildcards. * stands for any part of a name inside one folder, ** for any number of folders, and ? for one character, so res://images/ui/** is every file under images/ui/, however deep. A pattern belongs to the place whose place.toml it is in.

Keep the list short. Everything on it is waited for behind the loading screen, so a whole music folder there brings the long wait back. A track your menu plays a few seconds in is better left out: the sound starts once its file arrives.

preload only changes the published game. In the editor and in an exported game every file is already there.

Folders#

The rest of the place is four folders, and which side reads a file depends only on which one it is in:

place/
  place.toml
  server/     only the server reads these
  client/     only clients read these
  shared/     both sides read these
  scenes/     .scene files

require#

require loads another file and hands you back what it returns. There are two ways to name the file:

luau
local palette = require("@shared/palette")
local same = require("res://shared/palette.luau")
  • @server/, @client/ and @shared/ name those folders, and the extension can be left out. Use this form: it is the one Luau editors follow, so the module's own types reach the caller.
  • A res:// path starts at the place root and includes .luau. It cannot go up a folder: a .. segment, as in res://server/../secret.luau, is refused, so a path never leaves the place. (This is about paths only; .. joining strings works as usual.)
  • Both forms name the same file, so they share one loaded module.
  • A module runs once per side, and every caller gets the same value back.
  • A module returns exactly one value.
  • Two modules that require each other is an error: move what they share into a third.
  • A module must not task.wait while loading. Start the waiting from a task.spawn inside it.
  • Saving a module reloads the place, like any other file.
  • require also takes a ModuleScript instance.

A client script may require client/ and shared/, never server/. Server code never reaches a player's machine.

Reading files#

files.read hands a script the text of a file in the place, for data that is easier to keep as it came than to turn into a module: a chart in JSON, a sprite atlas in XML, a level written as text.

luau
-- client
local chart = http.jsonDecode(files.read("res://data/charts/bopeebo.json") :: string)
local atlas = xml.decode(files.read("res://images/BOYFRIEND.xml") :: string)

for _, frame in atlas.children do
    print(frame.attributes.name, tonumber(frame.attributes.x), tonumber(frame.attributes.y))
end
  • files.read(path) is the whole file as a string, or nil when there is no such file. files.exists(path) says whether there is one.
  • The path is a res:// path from the place root, like a require. A .. segment, an absolute path, or a link that leads out of the place is refused.
  • It only reads. There is no files.write.
  • A file can be at most 8 MB.
  • It reads the place on the machine the script runs on. Every player has the place (an exported game carries it, and a published game downloads it when the player joins), so the same call works in the editor, in an exported game, on a server, and on a player who joined one.
  • In a published game a file may still be downloading when you read it. The read then waits for it, up to 30 seconds, and the game holds still meanwhile. Data files come before the player joins, so this only happens for a sound, a video or a big picture read as bytes. A read that runs out of time is an error.
  • A published game's players get your client and shared scripts compiled, so reading one of them on a player's machine gives the bytecode, not the text. See Publishing a game.
  • A client script can not read server/, the same as require. .moud/ is the engine's own and is refused on both sides.
  • It reads the file again every call, so keep what you read instead of reading it every frame. Saving a JSON or XML file does not reload the place; the next read just sees the new text.
  • A UTF-8 byte order mark at the start is dropped.

JSON and XML#

http.jsonDecode turns JSON into tables, see Helpers. xml.decode turns XML into nested tables, one per element:

luau
local root = xml.decode([[<TextureAtlas imagePath="bf.png"><SubTexture name="idle0000" x="0" y="0"/></TextureAtlas>]])
root.tag                       -- "TextureAtlas"
root.attributes.imagePath      -- "bf.png"
root.children[1].tag           -- "SubTexture"
root.children[1].attributes.x  -- "0", attributes are always text
Field
tag the element's name
attributes its attributes, name to text. Turn numbers back with tonumber
children the elements inside it, in order
text the text directly inside it, CDATA included, with the space around it trimmed
  • Comments and <?xml ... ?> are skipped. &amp; and the other built in entities are turned back into their characters.
  • A DOCTYPE is not followed and an entity it defines is an error, so a file can not pull in another.
  • Broken XML is an error that names the line.

Scenes#

A scene file is a branch of the tree saved as JSON: classes, names, the properties that differ from their defaults, tags, children, and references inside the branch. You lay a scene out in the editor instead of writing it by hand, and a script loads it when it is needed.

luau
-- server
local built = scene.load("res://scenes/arena.scene")          -- under game.world
local more = scene.load("res://scenes/props.scene", folder)   -- under another instance

scene.save({ game.world:find("arena") }, "res://scenes/arena.scene")

local text = scene.encode({ game.world:find("arena") })
local copies = scene.decode(text, game.world)
  • scene.load parents what it builds under game.world, or under the instance you pass as the second argument.
  • scene.save writes the instances you list to a file, and only writes .scene files.
  • scene.encode gives you the same content as text, and scene.decode builds it again under the instance you name.

Scenes are what the editor reads and writes, which is why everything the engine can do is an instance: a light, a post effect, a chat window, a zone and a collision group all save with the scene they are in.

Terrain#

A scene's blocks live next to it: scenes/arena.scene uses scenes/arena.polar. A place from before this keeps its world.polar at the root, and only the start scene uses it.

  • File > Import Minecraft World turns a Java Edition world (any version, it is upgraded on the way) into the open scene's .polar. One scene holds one terrain, so importing replaces it; the old file goes to .moud/backups. The import is a step in the scene's history: Edit > Undo puts the old terrain back, and redo the imported one.
  • Opening a scene in the editor swaps the terrain around you. Renaming, moving, duplicating or deleting a scene in Assets takes its .polar along.
  • Only finished chunks are imported. Block entities such as chest contents are left out.

In the editor#

  • Scenes open in tabs above the viewport. Set as start scene writes [entry] scene.
  • Ctrl+G groups the selection. When everything selected has a position (parts, lights, attachments, other models) it goes into a Model. When the selection holds a gui or a script, or sits inside a viewport frame or a gui, it goes into a Folder. See Physics.
  • While you edit, the scene is copied into .moud/backups every minute it changed, and the saved file is copied before each save. The last 20 are kept, File > Scene Backups restores one, and opening a scene with a newer autosave than its file offers to recover it.

Exporting#

File > Export builds <id>-<version>.jar: the engine without the editor, Amnetic, Bkun, Resona, the addons the place uses and the place. Put it in the mods folder of a Minecraft with Fabric Loader and Fabric API, and launching goes straight into the place, unpacked into moud-games/<id>.

Or choose Modrinth pack: a .mrpack that Prism Launcher and the Modrinth App import as a ready instance, with Fabric Loader and Fabric API already set up. It holds the same jar plus Fabric API.

What players get depends on where the game runs. A game with a [server] section is played on your server, and its server scripts should stay there: they decide what players are allowed to do, and a player who could read them could cheat. So the copy players get leaves out what only the server runs: everything under server/, the server entry script and the [generation] script wherever they are, and the code of every Script in a scene. Keep a module only the server needs under server/ so it stays out too. shared/ and client/ go to every player.

The server itself runs from a Server jar, the third format, which holds all of the place. See Hosting a game.

A game without [server] runs on the player's own machine, server scripts included, so its jar and pack hold all of the place.

Before building, the export checks that the entry scripts and the start scene exist and that every res:// path written in scripts and scenes points at a file.

An addon is included when a place's entry script is in its language, when a scene or an add("...") call uses a class it registers, or when place.toml names it with [addons.<id>]. The rest are left out, so a Luau place carries neither Revo nor Java. The dialog lists each addon and why, and an addon the place needs that is not installed stops the export. See Addons in an exported game.

A place with a video file in it also gets ffmpeg, which a VideoFrame needs to play it. ffmpeg is the heaviest part of the engine, so a place without videos is exported without it.

The engine runs Luau and physics through native libraries, and a game only starts on a system it carries them for. The export dialog lists those systems before it builds. A game carries what the engine that exported it carries: the released engine has Linux x64, Windows x64 and macOS on both Intel and Apple silicon. An engine built from the repository has Luau for all four and the physics libraries ./gradlew allNatives built in box3d-java; build it with ./gradlew :mod:build -PallPlatforms so the Revo addon comes for every system as well.

Running a server#

A dedicated Minecraft server with Fabric and Moud runs a place when its Java command line names the place folder with -Dmoud.place=/path/to/place.

  • The server builds its world from the place, not from generated terrain. The level is empty void with the blocks of the start scene's .polar in it, and nothing else. A place with no terrain file gets an empty level.
  • This only happens when the server creates its world. A world folder that already exists, say one a server made before without a place, keeps the generator it was made with: delete the folder level-name names in server.properties (world by default) and start again.
  • Nothing is saved while a place runs, and nothing saved is read back. Blocks, level.dat, the world's data files, and each player's inventory, position, stats and advancements stay as they were on disk, and chunks come fresh from the generator every start. Keep what has to last with a store, which is written. /save-all and stopping the server write nothing.
  • A server started without a place, and a world opened from Minecraft's singleplayer list, are plain Minecraft and save as usual.
  • Nobody is kicked for flying while a place runs. The vanilla server can not see parts, so a player standing on one looks to it like a player floating in the air, and Moud turns that check off.

Hosting a game#

A server runs an exported game the same way a player's game does. In File > Export, choose Server jar for hosting. Put the <id>-<version>-server.jar it builds and Fabric API in the mods folder of a Fabric server for the same Minecraft version, and the server unpacks the place and runs it, with no -Dmoud.place needed.

Players get the jar or the Modrinth pack of the same version, since each player reads the place's client scripts, scenes and assets from their own copy. The two are not interchangeable:

  • The server jar holds the server scripts. Keep it on the server and never hand it to players.
  • The players' copy of a game with [server] has no server scripts, so a server started with it stops at once and says to put the server jar there instead.

A few things to set up on the server:

  • Leave online-mode=true in server.properties for a public server, so players sign in with their Microsoft account. false is for testing on your own machine.
  • Players reach the server on its port, 25565 unless server-port says otherwise. Open that port on your router, rent a server that takes Fabric mods, or run a tunnel.
  • The server owns the game: what the server script makes is what every player sees. Player input and requests reach it through remotes, which it checks before it acts on them.

This runs one server, which is all a game played with friends needs. A game on the Moud catalog runs on a fleet instead: it starts as many servers as players need, moves players between the places of a game, and closes the servers nobody is on. See Running a fleet.

Sending players to your server#

A multiplayer game tells its export where the server is with a [server] section in place.toml:

toml
[server]
lookup = "https://mygame.dev/moud.json"

The exported game then skips its own world and the title screen. It shows the loading screen with "Connecting to" and the game's name, reads the lookup file, and joins the first server in it that answers. When none answers it says why, with Try again and Quit. When the server kicks the player or the connection drops, the game comes back to that screen instead of Minecraft's menus. A loading script follows each step through game.loading and can draw all of these screens itself.

lookup is a link to a small JSON file you host. The game reads it every time it starts, so moving the server to a new address is editing that file, with no new export. lookup must be https://; plain http:// is only taken for localhost, to test. A server that never moves can skip the file:

toml
[server]
address = "play.mygame.net"      # or "51.12.34.56:25565"

The lookup file:

json
{
  "servers": ["play.mygame.net", "51.12.34.56:25565"],
  "version": "1.3.0",
  "download": "https://mygame.dev/download",
  "message": ""
}
Key
servers addresses to try in order, host or host:port. A name without a port is looked up like Minecraft's server list does, so an SRV record works. A single "address": "..." works too
version the oldest game version that may join. A game whose version in place.toml is older shows This game needs an update with a Download button instead of connecting
download the page Download opens in the browser
message shown under "Connecting to" while the game connects, for notices like planned maintenance
open false shows only the message, with Try again, and does not connect. Leave it out otherwise

Keys the game does not know are skipped, so a later engine can add some without breaking old games. The file may be at most 64 KB, and the game waits 5 seconds for it.

Any host that serves a static file over https works: GitHub Pages, a raw file in a GitHub repository, a gist's raw link, or your own site. Point lookup at the raw file, not the page that shows it.

  • To move the server, change servers and save the file. Players pick the new address up the next time they start the game.
  • To ship an update players must take, export the new version, put it where download points, then raise version to it.
  • For maintenance, set message, or "open": false to keep everyone out until you set it back.

The game keeps the last lookup it read in .moud/server.json inside moud-games/<id>. When the lookup can not be reached, it connects with that copy and says so under "Connecting to". A player who never reached the lookup has no copy and gets the reason with Try again.

File > Export reads the lookup once and lists what it found in its checks: the servers, the message, and a warning when the lookup asks for a newer version than the one being exported. A lookup it can not reach is a warning, not a stop. The same fields are under Server in Project Settings.

The server itself ignores [server]: it is the server.

From the repository#

Gradle starts a development server and clients that join it. The server needs a folder of its own with an eula.txt holding eula=true and a server.properties with at least:

properties
online-mode=false
server-port=25570

online-mode=false lets development clients in without a Microsoft account. Then:

./gradlew :mod:runServer -PrunDir=$HOME/moud/server -Pplace=$HOME/moud/arena
./gradlew :mod:runClient -PrunDir=$HOME/moud/alice -Pplace=$HOME/moud/arena -Pjoin=localhost:25570 -Pplayer=Alice
./gradlew :mod:runClient -PrunDir=$HOME/moud/bob -Pplace=$HOME/moud/arena -Pjoin=localhost:25570 -Pplayer=Bob
-PrunDir the folder the game or server runs in. It defaults to Moud-run beside the repository, so give each server and each client its own
-Pplace the place folder, passed on as moud.place
-Pjoin host:port of a server to join straight away, instead of opening the title screen
-Pplayer the name the client plays under, so two clients on one machine are two players
-Pconnect connect the way an export with a [server] section does, through its lookup or address, to test it without exporting. Passed on as moud.connect=true

A relative -PrunDir or -Pplace counts from the repository root.

Scripts as instances#

A Script is an instance that holds server code, and a LocalScript is one that holds client code. Each runs while it is in the tree and enabled, which lets you turn code on and off by moving it or by writing one property.

luau
world:add("Script", { name = "spinner", source = "res://server/spinner.luau" })
world:add("Script", { name = "hello", code = "print('hi from', script.name)" })
  • source is a file under server/ or shared/ (client/ or shared/ for a LocalScript).
  • code is the code itself and is used instead of source when set. It is never sent to a client.
  • Inside the script, script is the instance. local part = script.parent :: Part gives it a type.
  • In the editor, right-click an instance for Add script: it writes server/scripts/<Name>.luau from a template (empty, on touched, spin, kill on touch) and adds a Script pointing at it. Double-clicking a script opens it in Zed.
  • Destroying or disabling the instance stops everything it connected or scheduled: its signal connections, its task threads and its tweens. Moving it keeps it running.
  • A script inside ServerStorage, ReplicatedStorage, a StarterPack or StarterCharacterScripts does not run. Moving it in stops it like disabling it. See Containers.

ModuleScript#

A ModuleScript is a module that lives in the tree, so a script can require it by where it is rather than by a path. It never runs on its own.

luau
-- a script inside game.world.lib
local weapons = require(script.parent.weapons)
local same = require(game.world.lib.weapons)
print(weapons.damage("sword"))   -- 12
luau
-- weapons, a ModuleScript beside the script
local weapons = {}

function weapons.damage(name)
    return if name == "sword" then 12 else 4
end

return weapons

Both lines in the first sample reach the same module: script.parent.weapons walks up from the script that is running, and game.world.lib.weapons names the same instance from the root.

  • It has source and code, like a Script. code is used when set; source names a .luau file otherwise. One with neither is an error when required.
  • It follows the rules of require: it runs once per side, returns one value, and a client may not require a source under server/.
  • Every ModuleScript is loaded once per instance, whether it carries code or a source. Two modules pointing at the same file each run their own copy of it, and require("@shared/weapons") by path is a third. So script inside a module is always that ModuleScript, and script.parent is where it sits.
  • code reaches clients only when the module can be theirs: inside a LocalScript, ReplicatedStorage, StarterGui, StarterPlayerScripts or StarterCharacterScripts, or when its source is a file under client/ or shared/. Inside ServerStorage or ServerScriptService it never crosses, whatever else is true. Anywhere else it stays on the server. See Containers.
  • Requiring anything else, like a Folder, is an error that says a ModuleScript was expected.
  • In the editor, right-click an instance for Add module script: it writes shared/modules/Module.luau (Module2.luau and so on if that exists) and adds a ModuleScript pointing at it.