kustom-mc
v1.0.3
Published
CLI and type library for kustompack development
Maintainers
Readme
Kustom MC
CLI and type library for developing kustompack scripts with TypeScript support.
Installation
npm install kustom-mc
# or
npx kustom-mc <command>Quick Start
# Create a new project
npx kustom-mc init my-pack
# Build scripts
npx kustom-mc build
# Build and watch for changes
npx kustom-mc build --watch
# Push to development server
npx kustom-mc pushTable of Contents
- Script Format
- Props System
- Importing Scripts
- Screen GUI System
- Containers & Layouts
- Elements
- Chat GUI System
- Camera System
- Custom Blocks (Shaper)
- Custom Items
- BetterModel Entities
- Commands
- Configuration
Script Format
Scripts use the defineScript helper for type safety:
import { defineScript, Props } from "kustom-mc";
export default defineScript({
props: {
player: Props.String(),
message: Props.String("Hello!"),
count: Props.Number(1),
},
run() {
const player = this.getPlayer(this.props.player);
player.sendMessage(this.props.message);
this.exit("done");
},
});Script Context
The run function uses this to access the script context:
| Property / Method | Description |
|----------|-------------|
| this.props | Validated props passed to the script |
| this.executor | Who executed the script (player or console) |
| this.methods | Methods defined in defineScript({ methods }) |
| this.exit(value) | Exit with return value |
| this.emit(event, ...data) | Emit event to parent |
| this.getPlayer(name) | Get player by name |
| this.on(event, fn) | Register event listener |
| this.once(event, fn) | Register one-time listener |
| this.off(event, fn?) | Remove event listener(s) |
| this.open(script, props?) | Open a sub-process |
| this.call(method, ...args) | Call a named method |
| this.putData(key, value) | Store data on process |
| this.getData(key) | Get stored data |
| this.getId() | Get process ID |
Standalone classes available as globals:
new Camera(x, y, z) // Cinematic camera
new ChatGUI() // Chat-based UI
new ChatSession(id, cb) // Chat session
new Storage(namespace?) // Persistent storage
new Screen(size, options?) // Inventory GUIProps System
Define typed properties for your scripts:
import { defineScript, Props } from "kustom-mc";
export default defineScript({
props: {
// Required string (no default)
player: Props.String(),
// String with default
title: Props.String("Welcome"),
// Number with default
amount: Props.Number(100),
// Boolean with default
enabled: Props.Boolean(true),
// Object/Array
data: Props.Object({}),
items: Props.Array([]),
// Union type (one of these values)
mode: Props.Union("easy", "normal", "hard"),
},
run() {
console.log(this.props.player); // string
console.log(this.props.amount); // number
console.log(this.props.enabled); // boolean
},
});Typed Events System
Scripts can declare typed events for type-safe communication between parent and child scripts:
// digicode.ts
import { defineScript, Props } from "kustom-mc";
export default defineScript({
props: {
player: Props.String(),
code: Props.String("1234"),
},
// Declare events with typed payloads
events: {
onSuccess: Props.Boolean(), // Event with boolean payload
onUnlock: Props.String(), // Event with string payload
},
run() {
// Emit typed events - TypeScript checks payload types!
this.emit("onSuccess", true); // OK
this.emit("onUnlock", "1234"); // OK
// this.emit("onSuccess", "wrong"); // TS Error: expected boolean
// this.emit("unknown", true); // TS Error: unknown event
},
});// main.ts
import { defineScript, Props } from "kustom-mc";
import digicode from "./digicode";
export default defineScript({
props: {
player: Props.String(),
},
run() {
const child = digicode.run({
player: this.props.player,
code: "1234",
});
// TypeScript infers callback parameter types from digicode's events!
child.on("onSuccess", (value) => {
// value is typed as boolean
console.log("Code accepted!", value);
});
child.on("onUnlock", (code) => {
// code is typed as string
console.log("Entered code:", code);
});
// Use then() for exit handling (not events)
child.then((result) => {
console.log("Child exited with:", result);
});
},
});Event Methods
Scripts without events declaration still work - events are untyped (backward compatible).
const child = otherScript.run({ ...props });
// Listen for events
child.on("eventName", (value) => {}); // Listen for event
child.once("eventName", (value) => {}); // Listen once, auto-remove after first call
child.off("eventName", callback); // Remove specific listener
child.off("eventName"); // Remove all listeners for eventMultiple Listeners
Multiple listeners can be registered for the same event:
child.on("onSuccess", () => console.log("First handler"));
child.on("onSuccess", () => console.log("Second handler"));
// Both handlers are called when onSuccess is emittedImporting Scripts
Scripts can import and run other scripts:
ScriptProcess API
When running an imported script, you get a ScriptProcess:
const child = otherScript.run({ ...props });
child.on("eventName", (value) => {}); // Listen for events (typed if declared)
child.once("eventName", (value) => {}); // Listen once (auto-remove)
child.off("eventName", callback); // Remove specific listener
child.off("eventName"); // Remove all listeners for event
child.emit("eventName", ...args); // Send event to child
child.call("methodName", ...args); // Call method on child
child.then((result) => {}); // Handle completion
child.catch((error) => {}); // Handle errors
child.getId(); // Get process ID
child.hasExited(); // Check if completedScreen GUI System
Create inventory-based GUIs:
import { defineScript, Screen, Props } from "kustom-mc";
export default defineScript({
props: { player: Props.String() },
run() {
const player = this.getPlayer(this.props.player);
// Create a 27-slot screen (3 rows)
const screen = new Screen(27, {
usePlayerInventorySlots: true, // Use player inv for GUI components
persistenceKey: "my-gui", // Enable persistence
});
// Or with custom texture
const texturedScreen = new Screen("my_gui", 54);
// Add text overlay
const title = screen.appendText("My GUI", 12, -30, true);
// Add buttons
screen.addButton(13, "confirm_button", "Click me!", (event) => {
console.log("Button clicked!");
event.cancel(); // Prevent item pickup
});
// Handle events
screen.on("open", (p) => console.log(p.getName() + " opened"));
screen.on("close", (p) => {
this.exit(null);
});
// Open for player
screen.open(player);
},
});Screen Options
new Screen(size, {
persistenceKey: string, // Enable persistence with this key
usePlayerInventorySlots: boolean, // Use player inv for GUI components
id: string, // Unique identifier for persistence path
persist: boolean, // Enable content slot persistence
persistScroll: boolean, // Enable scroll position persistence
});Player Inventory Slots
Use player inventory slots for extended GUIs (like numpad):
const screen = new Screen(9, {
usePlayerInventorySlots: true, // Enable player inv slots
});
// Chest slots: 0-8 (for 9-slot screen)
// Player inventory slots: 54-89
// Mapping: main inv (9-35) -> 54-80, hotbar (0-8) -> 81-89
screen.addButton(54, "button_1", null, () => addDigit(1));
screen.addButton(81, "confirm", null, () => confirm());Containers & Layouts
Organize elements in containers with grid or flex layouts:
// Create a scrollable container
const container = screen.createContainer({
startSlot: 10,
rows: 3,
cols: 7,
scrollable: true,
maxCapacity: 100,
});
// Add items dynamically
for (let i = 0; i < 50; i++) {
container.pushButton("item_" + i, "Item " + i, (e) => {
console.log("Clicked item", i);
});
}
// Scroll controls
screen.addButton(8, "arrow_right", "Next", () => {
container.scrollPage(1);
screen.refresh();
});
screen.addButton(0, "arrow_left", "Previous", () => {
container.scrollPage(-1);
screen.refresh();
});
// Check pagination state
container.hasNextPage(); // boolean
container.hasPreviousPage(); // boolean
container.getCurrentPage(); // 0-based page number
container.getPageCount(); // total pagesContainer Methods
// Adding elements
container.pushButton(texture, title, onClick); // Next available slot
container.putButton(index, texture, title, onClick); // Specific index
container.pushContentSlot(); // Slot for player items
container.pushCover(); // Visual cover element
container.pushContainer(config); // Nested container
// Layout configuration (chainable)
container
.setLayout("flex") // "grid" or "flex"
.setDirection("row") // "row" or "column"
.setGap(1) // Gap between items
.setRows(3)
.setCols(7);
// Scrolling
container.scroll(offset); // Relative scroll
container.scrollTo(index); // Scroll to specific item
container.scrollToPage(page); // Go to page
// Events
container.on("scroll", (offset, page) => {});
container.on("childAdded", (child, index) => {});
container.on("capacityReached", () => {});Elements
Create different types of GUI elements:
import { Element } from "kustom-mc";
// Button element
const button = Element.button("my_texture", "Button Title");
button.onClick((e) => console.log("Clicked!"));
button.onRightClick((e) => console.log("Right clicked!"));
button.onShiftClick((e) => console.log("Shift clicked!"));
// Display-only element (no interaction)
const display = Element.display("info_icon");
// Content slot (accepts player items)
const slot = Element.contentSlot();
slot.on("afterItemPlace", (item, player, index) => {
console.log(player.getName() + " placed " + item.getMaterialName());
});
slot.on("afterItemTake", (player, index) => {
console.log(player.getName() + " took item from slot " + index);
});
// Cover element (visual overlay)
const cover = Element.cover();
cover.setColor("#FF0000"); // Red coverElement Chainable Methods
element
.setTexture("new_texture")
.setTitle("New Title")
.setLore(["Line 1", "Line 2"])
.setHideTooltip(true)
.setInteractive(false)
.setData({ custom: "data" });Chat GUI System
Create chat-based interactive UIs:
run() {
const gui = new ChatGUI();
gui.addLine((line) => {
line.center()
.background("#1a1a1a")
.addBoldText("Welcome!", "gold");
});
gui.addEmptyLine();
gui.addLine((line) => {
line.spaceBetween()
.addButton("[Accept]", "/accept", "Click to accept", "green")
.addButton("[Decline]", "/decline", "Click to decline", "red");
});
gui.setBlocking(true); // Capture chat input
gui.onInput((playerName, message, guiId) => {
console.log(playerName + " typed: " + message);
});
gui.open(this.props.player);
}ChatGUILine Methods
line
.padding(10) // Add padding
.center() // Center content
.spaceBetween() // Space between items
.background("#1a1a1a") // Background color
.addText("Hello", "white") // Add text
.addBoldText("Bold!", "gold") // Bold text
.addButton("[Click]", "/cmd", "Hover text", "green");Camera System
Create cinematic camera views:
run() {
const player = this.getPlayer(this.props.player);
const loc = player.getLocation();
// Create camera at player's position
const cam = new Camera(loc.getX(), loc.getY() + 5, loc.getZ());
// Start spectating
cam.startCameraMode(this.props.player);
// Camera controls
cam.lookAt(0, 64, 0); // Look at coordinates
cam.lookAtPlayer("OtherPlayer"); // Look at player
cam.setRotation(90, -30); // Set yaw/pitch
cam.moveTo(100, 70, 100); // Teleport camera
// Transparent view (see through blocks)
cam.setTransparentView(true);
// Animations
const anim = new Animation();
anim.createAnimation()
.start(0, 64, 0)
.end(100, 64, 100)
.duration(5)
.camera(cam.getId())
.onEnd(() => console.log("Animation done!"))
.build()
.start();
// Stop spectating
cam.stopSpectating();
cam.remove();
}Custom Blocks (Shaper)
Define custom blocks with models and collision:
run() {
// Custom blocks are defined declaratively using defineBlock():
// See "Block Definition File" below for the recommended approach.
// Spawn instances via the block's auto-generated placement item,
// or programmatically from a defineBlock's event handlers.
}Block Definition File
// blocks/tower.ts
import { defineBlock } from "kustom-mc";
export default defineBlock({
id: "tower",
model: "kustom:block/tower",
collision: { width: 1, height: 3, depth: 1, cornerY: 0 },
autoPlace: true, // Auto-generate placement item
onClick(event) {
event.player.sendMessage("Tower clicked!");
},
onBreak(event) {
return false; // Prevent breaking
},
});Custom Items
Define custom items with events:
// items/magic_wand.ts
import { defineItem } from "kustom-mc";
export default defineItem({
id: "magic_wand",
material: "STICK",
model: "kustom:item/magic_wand",
displayName: "<gold>Magic Wand",
description: "A powerful magical artifact",
onRightClick(event) {
event.player.sendMessage("<rainbow>Woosh!</rainbow>");
},
onLeftClick(event) {
// Attack with wand
},
onDamageEntity(event) {
// Custom damage logic
},
});BetterModel Entities
Spawn and control animated entities:
run() {
// BetterModel entities are managed through the Java API.
// Entity spawning and control is handled server-side.
// Scripts can interact with entities through event callbacks
// in defineBlock/defineItem definitions.
}Commands
CLI Commands
kustom init [project-name]
Create a new kustompack project.
npx kustom-mc init my-pack
npx kustom-mc init # Initialize in current directory
npx kustom-mc init --force # Overwrite existing fileskustom build
Compile TypeScript to JavaScript.
npx kustom-mc build # Build all scripts
npx kustom-mc build --watch # Watch mode
npx kustom-mc build --no-validate # Skip validation
npx kustom-mc build --clean # Clean output before buildingkustom bundle
Create a distributable zip file.
npx kustom-mc bundle # Create kustompack.zip
npx kustom-mc bundle -o my-pack.zip # Custom output name
npx kustom-mc bundle --include-source # Include .ts fileskustom validate
Check scripts for errors.
npx kustom-mc validate # Validate all scripts
npx kustom-mc validate --strict # Fail on warningskustom new <type> <name>
Generate a new script from template.
npx kustom-mc new script my-script # General script
npx kustom-mc new block my-block # Block definition
npx kustom-mc new item my-item # Item definition
npx kustom-mc new gui my-gui # GUI scriptIn-Game Commands
Define commands declaratively using defineCommand:
import { defineCommand } from "kustom-mc/command";
export default defineCommand({
id: "mycommand",
description: "A custom command",
execute(ctx) {
if (ctx.player) {
ctx.player.sendMessage("Hello from mycommand!");
}
// Need full API (camera, shaper, items, etc.)? Run a script:
// ctx.run("my-script", { target: ctx.args[0] });
},
tabComplete(ctx) {
return ["option1", "option2"];
}
});Configuration
kustom.config.json
{
"include": [
"scripts/**/*.ts",
"blocks/**/*.ts",
"items/**/*.ts"
],
"exclude": [
"**/*.test.ts"
],
"outDir": "dist",
"lib": ["scripts/lib"],
"manifest": {
"id": "my-pack",
"name": "My Pack",
"version": "1.0.0",
"scope": "global"
},
"server": {
"url": "http://localhost:8765"
},
"bundle": {
"output": "dist/kustompack.zip",
"include": ["**/*.js", "textures/**/*", "gui/**/*", "models/**/*"]
}
}Configuration Options
| Option | Description |
|--------|-------------|
| include | Glob patterns for script files |
| exclude | Patterns to exclude |
| outDir | Output directory for compiled scripts |
| lib | Directories containing shared libraries |
| manifest.id | Unique pack identifier (required) |
| manifest.name | Human-readable pack name |
| manifest.version | Pack version (semver) |
| manifest.scope | Pack scope: "global", "world", or "player" |
| server.url | Server URL for kustom push deployment |
| bundle.output | Output path for bundle zip |
| bundle.include | Files to include in bundle |
Project Structure
my-kustom-pack/
├── kustom.config.json
├── tsconfig.json
├── package.json
├── scripts/
│ ├── lib/ # Shared utilities
│ │ └── utils.ts
│ ├── main.ts # Main scripts
│ └── digicode.ts
├── blocks/
│ └── tower.ts # Block definitions
├── items/
│ └── magic_wand.ts # Item definitions
├── gui/
│ └── buttons/ # GUI textures
├── textures/
│ └── item/ # Item textures
├── models/
│ └── block/ # Block models
└── dist/ # Compiled output
└── scripts/License
MIT
