Skip to content

Core Mod Guide

github-actions[bot] edited this page Jan 9, 2026 · 2 revisions

日本語版はこちら

Core Mod Guide

Create advanced mods using Rust compiled to WASM.


Overview

Core Mods are WebAssembly modules that:

  • Run inside the game process
  • Access game state via host functions
  • Subscribe to game events
  • Modify game behavior

Use cases:

  • Custom machine logic
  • Event-driven automation
  • Performance-critical mods

Prerequisites

  • Rust toolchain with wasm32-unknown-unknown target
  • Basic understanding of Rust
# Install WASM target
rustup target add wasm32-unknown-unknown

Quick Start

1. Create project structure

mods/my_core_mod/
├── mod.toml
├── src/
│   └── lib.rs
├── Cargo.toml
└── scripts/
    └── main.wasm    ← Built output

2. Create mod.toml

[mod]
id = "my_core_mod"
name = "My Core Mod"
version = "1.0.0"
author = "Your Name"
description = "A custom WASM mod"
type = "core"

[mod.dependencies]
base = ">=0.3.0"

3. Create Cargo.toml

[package]
name = "my_core_mod"
version = "1.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
# No dependencies required for basic mods

4. Create src/lib.rs

//! My Core Mod

// Host function declarations
extern "C" {
    fn host_log_info(ptr: *const u8, len: usize);
    fn host_log_error(ptr: *const u8, len: usize);
    fn host_subscribe_event(event_type: u32) -> i32;
    fn host_get_machine_state(entity_id: u64) -> i32;
    fn host_set_machine_enabled(entity_id: u64, enabled: i32) -> i32;
    fn host_get_inventory_slot(entity_id: u64, slot: u32) -> u64;
}

// Helper: Log info message
fn log_info(msg: &str) {
    unsafe {
        host_log_info(msg.as_ptr(), msg.len());
    }
}

// Helper: Log error message
fn log_error(msg: &str) {
    unsafe {
        host_log_error(msg.as_ptr(), msg.len());
    }
}

/// Called when mod is loaded
#[no_mangle]
pub extern "C" fn on_init() {
    log_info("My Core Mod initialized!");
}

/// Called every game tick (20Hz)
#[no_mangle]
pub extern "C" fn on_tick() {
    // Called every tick, keep this fast!
}

/// Called when mod is unloaded
#[no_mangle]
pub extern "C" fn on_shutdown() {
    log_info("My Core Mod shutting down");
}

5. Build

cd mods/my_core_mod
cargo build --target wasm32-unknown-unknown --release

# Copy to scripts folder
mkdir -p scripts
cp target/wasm32-unknown-unknown/release/my_core_mod.wasm scripts/main.wasm

6. Test

Launch the game. Check logs/game.log for:

[CoreMod] Loading: my_core_mod
[CoreMod] my_core_mod: My Core Mod initialized!

Host Functions

Logging

extern "C" {
    /// Log info message
    fn host_log_info(ptr: *const u8, len: usize);

    /// Log error message
    fn host_log_error(ptr: *const u8, len: usize);
}

Events

extern "C" {
    /// Subscribe to event type
    /// Returns: subscription ID (>= 0) or error (< 0)
    fn host_subscribe_event(event_type: u32) -> i32;
}

// Event types
const EVENT_MACHINE_COMPLETE: u32 = 1;
const EVENT_ITEM_CRAFTED: u32 = 2;
const EVENT_BLOCK_PLACED: u32 = 3;
const EVENT_BLOCK_REMOVED: u32 = 4;

Machine Control

extern "C" {
    /// Get machine state
    /// Returns: 0=Idle, 1=Processing, 2=WaitingInput, -1=Error
    fn host_get_machine_state(entity_id: u64) -> i32;

    /// Enable/disable machine
    /// Returns: 0=Success, -1=NotFound, -2=PermissionDenied
    fn host_set_machine_enabled(entity_id: u64, enabled: i32) -> i32;
}

Inventory

extern "C" {
    /// Get inventory slot contents
    /// Returns: upper 32 bits = item_id, lower 32 bits = count
    fn host_get_inventory_slot(entity_id: u64, slot: u32) -> u64;
}

fn get_slot_contents(entity_id: u64, slot: u32) -> (u32, u32) {
    let result = unsafe { host_get_inventory_slot(entity_id, slot) };
    let item_id = (result >> 32) as u32;
    let count = result as u32;
    (item_id, count)
}

Lifecycle

/// Called once when mod loads
#[no_mangle]
pub extern "C" fn on_init() {
    // Initialize state, subscribe to events
}

/// Called every game tick (50ms / 20Hz)
#[no_mangle]
pub extern "C" fn on_tick() {
    // Update logic (keep fast!)
}

/// Called when mod unloads
#[no_mangle]
pub extern "C" fn on_shutdown() {
    // Cleanup
}

/// Called when subscribed event fires
#[no_mangle]
pub extern "C" fn on_event(event_type: u32, data: u64) {
    // Handle event
}

Example: Auto-Pause

Pause machines when output is full:

static mut MONITORED_MACHINES: Vec<u64> = Vec::new();

#[no_mangle]
pub extern "C" fn on_init() {
    log_info("Auto-Pause mod loaded");
}

#[no_mangle]
pub extern "C" fn on_tick() {
    unsafe {
        for &entity_id in &MONITORED_MACHINES {
            let (item_id, count) = get_slot_contents(entity_id, 0);

            // If output slot has 64+ items, pause
            if count >= 64 {
                host_set_machine_enabled(entity_id, 0);
            } else {
                host_set_machine_enabled(entity_id, 1);
            }
        }
    }
}

/// Register a machine for monitoring
#[no_mangle]
pub extern "C" fn register_machine(entity_id: u64) {
    unsafe {
        if !MONITORED_MACHINES.contains(&entity_id) {
            MONITORED_MACHINES.push(entity_id);
        }
    }
}

Best Practices

1. Keep on_tick() fast

// Bad: Heavy computation every tick
#[no_mangle]
pub extern "C" fn on_tick() {
    for entity in all_entities() {  // Thousands of entities!
        complex_calculation(entity);
    }
}

// Good: Only process when needed
static mut TICK_COUNTER: u32 = 0;

#[no_mangle]
pub extern "C" fn on_tick() {
    unsafe {
        TICK_COUNTER += 1;
        if TICK_COUNTER % 20 == 0 {  // Every second
            do_periodic_work();
        }
    }
}

2. Use events instead of polling

// Bad: Check every tick
#[no_mangle]
pub extern "C" fn on_tick() {
    if machine_completed() {
        handle_completion();
    }
}

// Good: Subscribe to events
#[no_mangle]
pub extern "C" fn on_init() {
    unsafe { host_subscribe_event(EVENT_MACHINE_COMPLETE); }
}

#[no_mangle]
pub extern "C" fn on_event(event_type: u32, data: u64) {
    if event_type == EVENT_MACHINE_COMPLETE {
        handle_completion(data);
    }
}

3. Handle errors gracefully

fn get_machine_state_safe(entity_id: u64) -> Option<MachineState> {
    let result = unsafe { host_get_machine_state(entity_id) };
    match result {
        0 => Some(MachineState::Idle),
        1 => Some(MachineState::Processing),
        2 => Some(MachineState::WaitingInput),
        _ => None,  // Error or unknown
    }
}

Debugging

Use log functions

fn debug_machine(entity_id: u64) {
    let state = unsafe { host_get_machine_state(entity_id) };
    log_info(&format!("Machine {} state: {}", entity_id, state));
}

Check game logs

logs/game.log

Core Mod output is prefixed with [CoreMod].


Limitations

Limitation Reason
No file I/O Security sandbox
No networking Security sandbox
Limited memory 16MB default
No threading WASM single-threaded

See Also

Clone this wiki locally