Skip to content

Script Mod Guide

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

日本語版はこちら

Script Mod Guide

Build external tools that communicate with the game via WebSocket.


Overview

Script Mods are external programs that:

  • Connect to the game via WebSocket
  • Query game state (items, machines, recipes)
  • Optionally modify data at runtime

Use cases:

  • Map viewers
  • Statistics dashboards
  • Item databases
  • Automation scripts

Quick Start

1. Start the game

WebSocket server runs on ws://127.0.0.1:9877.

2. Connect and query

JavaScript (Node.js or browser):

const WebSocket = require('ws'); // Node.js only

const ws = new WebSocket('ws://127.0.0.1:9877');

ws.on('open', () => {
  console.log('Connected to game');

  // Request game version
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'game.version',
    params: {}
  }));
});

ws.on('message', (data) => {
  const response = JSON.parse(data);
  console.log('Response:', response.result);
});

Python:

import asyncio
import websockets
import json

async def main():
    uri = "ws://127.0.0.1:9877"
    async with websockets.connect(uri) as ws:
        # Request game version
        await ws.send(json.dumps({
            "jsonrpc": "2.0",
            "id": 1,
            "method": "game.version",
            "params": {}
        }))

        response = json.loads(await ws.recv())
        print(f"Game version: {response['result']['version']}")

asyncio.run(main())

Helper Library

JavaScript async wrapper

class GameClient {
  constructor(url = 'ws://127.0.0.1:9877') {
    this.ws = new WebSocket(url);
    this.pending = new Map();
    this.nextId = 1;

    this.ws.onmessage = (event) => {
      const response = JSON.parse(event.data);
      const resolve = this.pending.get(response.id);
      if (resolve) {
        this.pending.delete(response.id);
        resolve(response);
      }
    };
  }

  async ready() {
    return new Promise(resolve => {
      if (this.ws.readyState === 1) resolve();
      else this.ws.onopen = resolve;
    });
  }

  async call(method, params = {}) {
    await this.ready();
    const id = this.nextId++;

    return new Promise((resolve, reject) => {
      this.pending.set(id, (response) => {
        if (response.error) reject(response.error);
        else resolve(response.result);
      });

      this.ws.send(JSON.stringify({
        jsonrpc: '2.0',
        id,
        method,
        params
      }));
    });
  }
}

// Usage
const client = new GameClient();
const version = await client.call('game.version');
const items = await client.call('item.list');

Example: Item Database

const client = new GameClient();

async function buildItemDatabase() {
  const { items } = await client.call('item.list');

  const database = {
    byId: {},
    byCategory: {},
    byTag: {}
  };

  for (const item of items) {
    // Index by ID
    database.byId[item.id] = item;

    // Index by category
    if (!database.byCategory[item.category]) {
      database.byCategory[item.category] = [];
    }
    database.byCategory[item.category].push(item);

    // Index by tags (if available)
    for (const tag of item.tags || []) {
      if (!database.byTag[tag]) {
        database.byTag[tag] = [];
      }
      database.byTag[tag].push(item);
    }
  }

  return database;
}

// Usage
const db = await buildItemDatabase();
console.log('Ores:', db.byCategory['ore']);
console.log('Smeltable:', db.byTag['smeltable']);

Example: Recipe Analyzer

async function analyzeRecipes() {
  const { recipes } = await client.call('recipe.list');
  const { items } = await client.call('item.list');

  const itemMap = Object.fromEntries(items.map(i => [i.id, i]));

  for (const recipe of recipes) {
    const inputCost = recipe.inputs.reduce((sum, i) => sum + i.count, 0);
    const outputYield = recipe.outputs.reduce((sum, o) => sum + o.count, 0);
    const efficiency = outputYield / inputCost;

    console.log(`${recipe.id}: ${efficiency.toFixed(2)}x efficiency`);
  }
}

Example: Live Statistics

async function monitorGame() {
  setInterval(async () => {
    const state = await client.call('game.state');

    console.log(`Tick: ${state.tick}, Paused: ${state.paused}`);
  }, 1000);
}

Best Practices

1. Handle disconnection

ws.on('close', () => {
  console.log('Disconnected, reconnecting...');
  setTimeout(() => connect(), 5000);
});

ws.on('error', (err) => {
  console.error('WebSocket error:', err);
});

2. Batch requests

// Bad: Many sequential requests
for (const id of itemIds) {
  await client.call('item.info', { id });
}

// Good: Single list request
const { items } = await client.call('item.list');
const filtered = items.filter(i => itemIds.includes(i.id));

3. Cache data

let cachedItems = null;
let cacheTime = 0;

async function getItems() {
  if (!cachedItems || Date.now() - cacheTime > 60000) {
    cachedItems = await client.call('item.list');
    cacheTime = Date.now();
  }
  return cachedItems;
}

Available Methods

See WebSocket API for full reference.

Method Description
game.version Get game/API version
game.state Get current game state
item.list List all items
item.add Register new item
machine.list List all machines
machine.add Register new machine
recipe.list List all recipes
recipe.add Register new recipe
mod.list List loaded mods
mod.info Get mod details
mod.enable/disable Toggle mod state
texture.list List textures

Debugging

Check connection

# Using websocat (install: cargo install websocat)
echo '{"jsonrpc":"2.0","id":1,"method":"game.version","params":{}}' | websocat ws://127.0.0.1:9877

Check game logs

logs/game.log

WebSocket errors are logged with [WebSocket] prefix.


See Also

Clone this wiki locally