Skip to content
Oxillen Glow edited this page Jul 8, 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 or any other place to learn basic JavaScript.


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. Scripts that run on initialize that needs globals set by another, will probably have to prefix filenames: 00_lib.js, 01_feature.js.


Tip

To solve naming conflicts, use one global slot per mod.

Instead of:

getSomething() { return 42; }

Use:

globalThis.ExampleName = {
    getSomething() { return 42;}
};

Note

Anything could still change, I might just make it so that every folder has its own context.


Globals

Five 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 and block-change rules
Gui Create screen-space text elements
Player Read/set the player camera's position

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

A Block.place/Block.destroy call can be rejected — see Engine.onBlockChange below. A rejected call throws; if it happens inside an Engine.onTick callback, that counts toward the callback's failure count (see Engine).

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


Player

Read or move the player's camera directly.

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


Engine

Per-tick callbacks

Register a function to run on a specific tag group.

// 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. The main loop ticks exactly one group per frame — "Update" — not every registered tag. If you register a callback under any other tag, it will never fire on its own; it only runs when a Gui element 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.

Validating block changes

Register a function that runs on every Block.place / Block.destroy call (from any script, 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. Multiple validators can be registered; the first one to reject wins. There's no way to know which validator rejected a given change from JS — write validators to log their own rejections if you need that.


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 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
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. 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 also happened to be tagged "Update" and got clicked. Design callbacks to be idempotent where possible.


WorldGenerator

This is a must have in the game so it comes as a default. It also runs separately from the mods, just edit the https://github.com/OxillenGlow/MtSharpGrain/blob/main/worlds/my_world/chunkgen.js file to modify how your world is built.

Specialized Chunks

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)>/storageGround so that it shows up randomly inside the game as you walk around.


Full examples

Ground indicator

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

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

Block above player (demo)

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

Limitations

  • 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. Any other tag only runs when a matching Gui element is clicked.
  • 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(){ ... })()).
  • Player moves the camera with no collision checks — it's a teleport, not physics-driven movement.
  • Engine.onBlockChange validators run synchronously on every block edit — keep them cheap, since they run inline with Block.place/Block.destroy.
  • No filesystem, network, or Java class access from scripts. (unless i have a sudden urge to add them)
  • console.log output appears in the game's standard output. (currently only accessable by players via running with terminal)

Image MtSharpGrain


Clone this wiki locally