Skip to content
Oxillen Glow edited this page Jul 3, 2026 · 5 revisions

MtSharpGrain JS Modding

Scripts are plain JavaScript files placed anywhere under worlds/my_world/mod/. All .js files are loaded recursively on startup, sorted alphabetically. One bad script does not block the others — errors are logged and that script is skipped.

If you are new to JavaScript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide


File placement

worlds/
  my_world/
    mod/
      my_script.js        ← loaded
      ui/
        hud.js            ← also loaded
        inventory.js      ← also loaded

Load order is alphabetical within each folder, parent folders before subfolders. If one script needs globals set by another, prefix filenames: 00_lib.js, 01_feature.js.


Globals

Four objects are injected into every script automatically. 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
Gui Create screen-space text elements

Scene

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. Attach your objects there unless you have a specific parent in mind.

// 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);

Block

Read and write voxel blocks. Changes trigger a mesh rebuild for the affected chunk and its neighbors automatically.

// 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 air

Block IDs

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


Engine

Register a function to run on a specific tick group. The engine only ticks named groups — if your tag is not in the game's tick list, your callback only fires when a GUI element with that tag is clicked (see Gui below). Ask a developer to add your tag to the main tick loop if you need it to run every frame.

// 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
}, "myFloatingCube");

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.


Gui

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. toTop/toBottom reorder an existing element.

guiWord is upsert by tag — calling it again with the same tag updates the existing element instead of creating a duplicate. Call it from your tick callback every frame to animate text.

// guiWord(word, x, y, z, sizePixels, tag) → handle
// z is reserved for future use — 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
const h = Gui.getHandleByTag("myLabel");

Clickable GUI elements

Every GUI element is clickable. When clicked, the engine fires tick(tpf, tag) for that element's tag — so a button is just a guiWord whose tag matches a registered Engine.onTick callback:

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 — your function runs both when the group ticks normally and when the element is clicked. Design callbacks to be idempotent where possible.


Full example

// worlds/my_world/mod/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);

const label = Gui.guiWord("...", 0.5, 0.95, 0, 0.02, "groundLabel");

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");
    }
}, "groundCheckLabel");

Limitations

  • Scripts run on the render thread. Do not spin-wait or do heavy computation inside Engine.onTick — keep callbacks short.
  • Scripts share one GraalVM context. A global variable you declare in one script is visible in all scripts loaded after it. Use unique names or wrap in an IIFE ((function(){ ... })()).
  • No filesystem, network, or Java class access from scripts.
  • console.log output appears in the game's standard output.

Image MtSharpGrain


Clone this wiki locally