-
Notifications
You must be signed in to change notification settings - Fork 0
2.1 Code
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.
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.
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 |
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);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 airA 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).
| 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.
// 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.
// 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.
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.
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");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.
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.
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.
// 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");// 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");- 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 matchingGuielement 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(){ ... })()). -
Playermoves the camera with no collision checks — it's a teleport, not physics-driven movement. -
Engine.onBlockChangevalidators run synchronously on every block edit — keep them cheap, since they run inline withBlock.place/Block.destroy. - 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 accessable 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