-
Notifications
You must be signed in to change notification settings - Fork 0
2.1 Code
Scripts are organized into mod packs — each top-level subfolder under
worlds/my_world/mod/ gets its own isolated GraalVM Context. All .js
files within a pack's folder (including subfolders) are loaded recursively
on startup, sorted alphabetically. One bad script does not block others in
the same pack, or any other pack — errors are logged and that script is
skipped.
If you are new to JavaScript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide or any other place to learn basic JavaScript.
worlds/
my_world/
chunkgen.js ← Special file for generating the world,
mod/
loose.js ← NOT loaded (no pack folder = skipped)
ui/ ← mod pack folder "ui" — has its own Context
hud.js ← loaded into ui's context
inventory.js ← loaded into ui's context
widgets/
button.js ← still ui's context (subfolders included)
gameplay/ ← mod pack "gameplay" — separate Context
movement.js ← isolated from ui entirely
blocks.js
see here for more on chunkgen.js
-
Subdirectories only. A
.jsfile placed directly inmod/(not inside a subfolder) is not loaded. Every pack needs its own folder. -
One Context per pack.
uiandgameplayabove don't share globals, variables, or state. A crash or infinite loop in one pack does not affect another. -
Pack load order. Packs are initialized alphabetically by folder name
(
gameplaybeforeui). - File load order within a pack. Alphabetical, recursive through subfolders — same rule as before, just scoped to one pack now.
-
Init-order dependencies. If a script needs a global set by another
script in the same pack, prefix filenames:
00_lib.js,01_feature.js. This no longer works across packs — see below.
Tip
Globals don't leak between packs, so naming conflicts across different mods are no longer a concern the way they used to be.
In pack ui/:
globalThis.Toolbar = { ... };In pack gameplay/:
globalThis.Toolbar = { ... }; // totally separate, no collisionWithin a single pack, the old advice still applies — use one global slot per script to avoid stepping on your own other scripts:
globalThis.ExampleName = {
getSomething() { return 42; }
};Warning
Engine.onBlockChange validators are not pack-scoped in their effect.
Every pack's validators run on every block edit, and if any pack's
validator rejects the change, the whole edit is rejected — regardless of
which pack triggered it. There is currently no way to scope a validator to
only your own pack's edits. Write validators to fail open (return true)
unless you specifically mean to block something globally, and keep them
cheap since they all run inline on every Block.place/Block.destroy.
Five objects are injected into every script automatically, fresh per pack (each pack gets its own instances, not shared references). You cannot access arbitrary Java classes — only what is listed here.
| Global | Purpose |
|---|---|
Scene |
Create/move/destroy 3D objects in the world |
Block |
Read and edit voxel blocks |
Engine |
Register per-tick callbacks and block-change rules |
Gui |
Create screen-space text elements |
Player |
Read/set the player camera's position |
Note: Scene node handles, Gui element handles, and tick tags are only
valid within the pack that created them. A handle created in gameplay
means nothing to a script in ui — there's no cross-pack handoff mechanism
yet.
All Scene methods operate on handles — opaque numbers that refer to a scene node. Never try to store or manipulate the underlying Java object; always go through the handle.
Handle 0 is the world root node in every pack's context — each pack
sees the same root node, so objects from different packs still end up in
the same visible scene graph, they just can't reference each other's handles.
// createNode(name) → handle
const group = Scene.createNode("myGroup");
// createCube(name, size) → handle
// size is the full edge length, e.g. 1.0 = a standard voxel-sized cube
const cube = Scene.createCube("myCube", 1.0);
// attachChild(parentHandle, childHandle)
Scene.attachChild(0, group); // attach group to world root
Scene.attachChild(group, cube); // attach cube under group
// setPosition(handle, x, y, z)
Scene.setPosition(cube, 10, 5, 0);
// getPosition(handle) → [x, y, z] (world position, not local)
const p = Scene.getPosition(cube);
const wx = p[0], wy = p[1], wz = p[2];
// setColor(handle, r, g, b, a) — values 0..1
Scene.setColor(cube, 1, 0, 0, 1); // red
// destroy(handle) — removes from scene and frees the handle
Scene.destroy(cube);
// getBlockId(x, y, z) → int — read-only voxel query at integer block coords
const id = Scene.getBlockId(10, 4, 0);Read and write voxel blocks. Changes trigger a mesh rebuild for the affected
chunk and its neighbors automatically. Block calls affect the shared world
state — this is one of the few things that is effectively cross-pack,
since there's only one world.
// get(x, y, z) → int
const id = Block.get(10, 4, 0);
// place(x, y, z, blockId)
Block.place(10, 5, 0, 2); // place stone
// destroy(x, y, z)
Block.destroy(10, 5, 0); // set to airA Block.place/Block.destroy call can be rejected — see
Engine.onBlockChange below. This now runs
validators from every loaded pack, not just your own. A rejected call
throws; if it happens inside an Engine.onTick callback, that counts toward
the callback's failure count (see Engine).
| ID | Name |
|---|---|
| 0 | Air |
| 2 | Stone |
| 3 | Dirt |
| 4 | Grass |
| 5 | Crystal Ore |
| 6 | Ice Sludge |
| 7 | Silicon |
| 8 | Sulfur |
| 9 | Metal Block |
| 10 | Glass |
for the newest see this java class
Read or move the player's camera directly. Shared across packs — there's
only one player/camera, same as with Block.
// getPosition() → [x, y, z]
const p = Player.getPosition();
const px = p[0], py = p[1], pz = p[2];
// setPosition(x, y, z) — teleports the camera
Player.setPosition(0, 20, 0);Player wraps the game camera, not a separate physics body — moving it
moves the view instantly with no collision checks.
Register a function to run on a specific tag group. Tags are scoped to the
pack that registers them — a "placeStoneBtn" tag in gameplay/ and a
"placeStoneBtn" tag in ui/ are two entirely separate registrations, each
only dispatched within its own pack's context.
// Engine.onTick(fn, tag)
// fn receives (tpf, tag):
// tpf — time per frame in seconds (use for frame-rate independent movement)
// tag — the tag this callback was registered under
Engine.onTick(function(tpf, tag) {
const p = Scene.getPosition(cube);
Scene.setPosition(cube, p[0], p[1] + tpf, p[2]); // float upward
}, "Update");Only the "Update" tag runs automatically every frame, per pack. Each
pack's "Update" group ticks once per frame, independently of every other
pack's "Update" group. If you register a callback under any other tag, it
will never fire on its own; it only runs when a Gui element in the
same pack sharing that exact tag is clicked (see below). If you want
per-frame logic, tag it "Update". If you want a click handler, give it
whatever tag your button uses — don't tag a button "Update" unless you
also want it firing every single frame.
A tag can have multiple callbacks — all of them fire when that group ticks. Callbacks that throw 5 times in a row are automatically disabled with an error in the log.
Register a function that runs on every Block.place / Block.destroy call
(from any script in any pack, or from player-driven edits) and decides
whether it's allowed.
// Engine.onBlockChange(fn)
// fn receives (x, y, z, blockId) and must return true (allow) or false (reject)
Engine.onBlockChange(function(x, y, z, blockId) {
if (blockId === 9 && y > 200) return false; // no metal above y=200
return true;
});If a validator returns false — or throws — the change is rejected and the
call that triggered it (Block.place, Block.destroy, or a player edit)
throws instead of applying. This now applies across every pack: your
validator gets a say on edits made by scripts in other packs too, and their
validators get a say on yours. Multiple validators can be registered across
all packs; the first one to reject wins. There's no way to know which
validator (or which pack) rejected a given change from JS — write
validators to log their own rejections if you need that.
Create screen-space text. Coordinates are normalized: 0,0 = bottom-left,
1,1 = top-right. Draw order matches the element list — elements added later
draw on top, within the same pack. Elements from different packs are
drawn pack-by-pack (in the same alphabetical order packs load in), not
interleaved by creation time across packs. toTop/toBottom only reorder
within the calling pack's own element list.
guiWord is upsert by tag, scoped to the pack. Calling it again with the
same tag from the same pack updates the existing element instead of creating
a duplicate. A tag "myLabel" in ui/ and a tag "myLabel" in gameplay/
are two different elements. Call it from an "Update" tick callback if you
want it to animate every frame.
// guiWord(word, x, y, z, sizePixels, tag) → handle
// z is there because I got a bit carried over from all the 3d stuff — just pass 0 for now
const handle = Gui.guiWord("Hello world", 0.5, 0.9, 0, 0.02, "myLabel");
// setColor(handle, r, g, b, a)
Gui.setColor(handle, 0, 1, 0, 1); // green
// toTop(handle) / toBottom(handle) — reorder in draw list
Gui.toTop(handle);
Gui.toBottom(handle);
// removeWord(handle) — removes the element entirely
Gui.removeWord(handle);
// getHandleByTag(tag) → handle — look up a handle you may have lost
// (only finds handles created by this same pack)
const h = Gui.getHandleByTag("myLabel");Every GUI element is clickable. When clicked, the engine fires tick(tpf, tag)
for that element's tag within the same pack that created the element — so
a button is just a guiWord whose tag matches an Engine.onTick callback
registered in the same pack's script(s). This works independently of the
"Update" rule above — clicks dispatch directly to their tag, they don't need
to be named "Update":
const btn = Gui.guiWord("[ Place Stone ]", 0.5, 0.05, 0, 0.025, "placeStoneBtn");
Gui.setColor(btn, 0.4, 0.8, 1.0, 1.0);
Engine.onTick(function(tpf, tag) {
Block.place(10, 5, 0, 2);
Gui.setColor(btn, 0, 1, 0, 1); // flash green on click
}, "placeStoneBtn");Click dispatch and regular per-frame ticking share the same callback type —
a callback tagged "Update" runs every frame and would run again if a GUI
element in the same pack also happened to be tagged "Update" and got
clicked. Design callbacks to be idempotent where possible.
This is a must-have in the game so it comes as a default, and runs completely separately from mod packs — it is not part of the pack system at all, has no isolation concerns, and isn't affected by anything above. Just edit https://github.com/OxillenGlow/MtSharpGrain/blob/main/worlds/my_world/chunkgen.js to modify how your world is built.
You could also duplicate a chunk you have built on and put it in
worlds/<name(default is my_world)>/storageGround or
worlds/<name(default is my_world)>/storageAir so that it shows up randomly
inside the game as you walk around.
// worlds/my_world/mod/gameplay/ground_indicator.js
// Colors a cube green when over solid ground, red when over air.
const cube = Scene.createCube("indicator", 0.5);
Scene.attachChild(0, cube);
Scene.setPosition(cube, 10, 20, 0);
Engine.onTick(function(tpf, tag) {
const p = Scene.getPosition(cube);
const blockBelow = Scene.getBlockId(
Math.floor(p[0]),
Math.floor(p[1]) - 1,
Math.floor(p[2])
);
if (blockBelow !== 0) {
Scene.setColor(cube, 0, 1, 0, 1); // green — solid ground
Gui.guiWord("Ground: solid", 0.5, 0.95, 0, 0.02, "groundLabel");
} else {
Scene.setColor(cube, 1, 0, 0, 1); // red — air below
Gui.guiWord("Ground: air", 0.5, 0.95, 0, 0.02, "groundLabel");
}
}, "Update");// worlds/my_world/mod/gameplay/demo_block_above_player.js
// Every tick, sets the block 1 meter above the player to Glass (id 10).
Engine.onTick(function(tpf, tag) {
const p = Player.getPosition();
const x = Math.floor(p[0]);
const y = Math.floor(p[1]) + 1;
const z = Math.floor(p[2]);
Block.place(x, y, z, 10);
}, "Update");Note both examples now live under mod/gameplay/ rather than directly in
mod/ — a loose mod/ground_indicator.js would silently not load under the
new pack system.
- Scripts run on the render thread. Do not spin-wait or do heavy
computation inside
Engine.onTick— keep callbacks short. - Only the
"Update"tag is driven every frame by the engine, per pack. Any other tag only runs when a matchingGuielement in the same pack is clicked. - Each pack is its own GraalVM context. A global variable declared in one pack is not visible to scripts in another pack — only to other scripts within the same pack, in load order. This is a change from earlier versions where all mods shared one context; if you're upgrading old mods, check for cross-file globals that now need to move into the same pack folder.
- Loose
.jsfiles directly insidemod/(not in a subfolder) are not loaded at all — every mod needs a pack folder. -
Engine.onBlockChangevalidators still run synchronously on every block edit and are still cheap-only, but now that includes validators from every other pack too — a slow or misbehaving validator in one pack can affect edits triggered by any other pack. -
Playermoves the camera with no collision checks — it's a teleport, not physics-driven movement. - No filesystem, network, or Java class access from scripts. (unless i have a sudden urge to add them)
-
console.logoutput appears in the game's standard output. (currently only accessible by players via running with terminal)
- Home
- How to play?
- How to code modifiers using JavaScript?
- jVisualScripting (still in construction 🚧)
- Source Code
- Versions
- Downloads(not working yet) Scroll down to bottom of the release card and click the assets, select .jar/... and download. For more see install page
MtSharpGrain