ラベル Minecraft の投稿を表示しています。 すべての投稿を表示
ラベル Minecraft の投稿を表示しています。 すべての投稿を表示

2026年5月10日日曜日

Minecraft サーバに Timeless and Classics Zero Guns (TaCZ) をインストールする方法

Minecraft サーバに Timeless and Classics Zero Guns (TaCZ) をインストールする方法

概要

サーバにインストールしクライアント側にも同じ Mod のインストールが必要です
必ずバージョンは揃えましょう

環境

  • サーバ側
    • macOS 26.4.1
    • Minecraft 1.20.1
    • forge 47.4.10
    • TaCZ 1.1.8
  • クライアント側
    • macOS 26.4.1/Windows11Home
    • Minecraft 1.20.1
    • forge 47.4.10
    • TaCZ 1.1.8

サーバ側

compose.yaml

サーバ側は docker compose で起動します

  • vim compose.yaml
services:
  mc:
    image: itzg/minecraft-server
    tty: true
    stdin_open: true
    ports:
      - "25565:25565"
    environment:
      EULA: "TRUE"
      MODE: "creative"
      VERSION: "1.21.11"
    volumes:
      # attach the relative directory 'data' to the container's /data path
      - ./data:/data

  mc_mod:
    image: itzg/minecraft-server
    tty: true
    stdin_open: true
    ports:
      - "25566:25565"
    environment:
      EULA: "TRUE"
      MODE: "creative"
      VERSION: "1.20.1"
      TYPE: "forge"
    volumes:
      # modded world/data directory for the modded server
      - ./modded-data:/data
      # put the CurseForge mod JAR into this local directory
      - ./timeless_and_classics_zero/mods:/data/mods
      # optional config directory for mod/server configs
      - ./timeless_and_classics_zero/config:/data/config

通常のマイクラサーバとは別で TaCZ がインストールされた forge サーバを起動します

TaCZ のダウンロードとインストール

https://www.curseforge.com/minecraft/mc-mods/timeless-and-classics-zero

から tacz-1.20.1-1.1.8-release.jar をダウンロードします
サーバ側は timeless_and_classics_zero/mods 配下に配置します

  • mkdir timeless_and_classics_zero/mods
  • mkdir timeless_and_classics_zero/config
  • mkdir modded-data

起動確認

  • docker compose up -d

で問題なく起動することを確認します

クライアント側 (Mac)

forge のインストール

https://files.minecraftforge.net/net/minecraftforge/forge/index_1.20.1.html

から forge-1.20.1-47.4.10-installer.jar をダウンロードしダブルクリックでインストールします
デフォルトだと開けないのでプライバシーとセキュリティから jar を開きます

TaCZ のダウンロードとインストール

https://www.curseforge.com/minecraft/mc-mods/timeless-and-classics-zero

から tacz-1.20.1-1.1.8-release.jar をダウンロードします
Mac の場合のクライアントは以下のパスに Mod を配置します

/Users/username/Library/Application Support/minecraft/mods

起動確認

ランチャーを起動し1.20.1-forge-47.4.10 の起動構成を作成してその起動構成からマイクラを起動しましょう

あとはマルチプレイから起動した docker compose のサーバにアクセスすれば OK です

クライアント側 (Windows)

JDK のインストール

Oracle 版でも OpenJDK でも OK です

https://www.oracle.com/jp/java/technologies/downloads/

コマンドをプロンプトを起動して java コマンドで実行できるか確認します

forge のインストール

https://files.minecraftforge.net/net/minecraftforge/forge/index_1.20.1.html

から forge-1.20.1-47.4.10-installer.jar をダウンロードします
ダブルクリックだとインストールできないのでコマンドプロンプトを開いて起動します

  • java -jar forge-1.20.1-47.4.10-installer.jar

TaCZ のダウンロードとインストール

https://www.curseforge.com/minecraft/mc-mods/timeless-and-classics-zero

から tacz-1.20.1-1.1.8-release.jar をダウンロードします
Windows の場合のクライアントは以下のパスに Mod を配置します

C:\Users\xxx\AppData\Roaming\.minecraft\mods

起動確認

ランチャーを起動し1.20.1-forge-47.4.10 の起動構成を作成してその起動構成からマイクラを起動しましょう

あとはマルチプレイから起動した docker compose のサーバにアクセスすれば OK です

最後に

マルチで Mod を使って遊ぶ場合にはサーバ側とクライアント側に同じバージョンの同じ Mod をインストールする必要があるので注意しましょう

2025年8月16日土曜日

Mineflayer と ollama を連携して自然言語でボットを制御してみた

Mineflayer と ollama を連携して自然言語でボットを制御してみた

概要

前回 Mineflayer 上で JavaScript の eval を使って動的にコードを動かすことに成功しました
今度はその応用として AI (ollama) にコードを生成してもらい生成したコードをボットに実行してもらうようにしてみました

環境

  • macOS 15.6
  • Minecraft 1.21.4
  • nodejs 22.15.1
  • mineflayer 4.31.0

lib/command/commands.js

useAiCodeCmd を追加しています

node-fetch を使って ollama の API をコールします
コールしたあとでレスポンスはそのまま使わずに少し加工しています
サンプルでは gemma3 を使っていますがこのあたりのレスポンスの処理はモデルによって変わってくるので適宜変更してください

const { VM } = require("vm2");
const fetch = require("node-fetch");
const { goals, Movements } = require("mineflayer-pathfinder");
const { GoalBlock } = goals;
const {
  buildBaseBlock,
  buildFloorFromBase,
  buildWallsFromBase,
  buildRoofFlatFromBase,
  moveBotToCenter,
} = require("./build/house");
const { tryRescue } = require("./dig/low_level_api");

const mcDataLoader = require("minecraft-data");

// 指定の座標に移動するコマンド
function moveCmd(bot, username, args) {
  if (args.length < 3) {
    bot.chat(`Usage: move <x> <y> <z>`);
    return;
  }

  const [x, y, z] = args.map(Number);
  if ([x, y, z].some(isNaN)) {
    bot.chat("座標は数値で指定してください");
    return;
  }

  bot.chat(`${username} の指示で (${x}, ${y}, ${z}) に移動します`);
  bot.pathfinder.setGoal(new GoalBlock(x, y, z));
}

// プレイヤーの目の前に移動するコマンド(canDig + 自動救出版)
function comeOnCmd(bot, username) {
  function getScaffoldBlocks(bot, mcData) {
    const usableItemIds = [
      mcData.itemsByName.dirt.id,
      mcData.itemsByName.cobblestone.id,
      mcData.itemsByName.sand.id,
    ];
    return bot.inventory
      .items()
      .filter((item) => usableItemIds.includes(item.type))
      .map((item) => item.type);
  }

  const player = bot.players[username]?.entity;
  if (!player) {
    bot.chat(`プレイヤー ${username} が見つかりません`);
    return;
  }

  const mcData = mcDataLoader(bot.version);
  const movements = new Movements(bot, mcData);

  // 各種設定
  movements.allowParkour = true;
  movements.allow1by1towers = true;
  movements.canDig = true;
  movements.scafoldingBlocks = getScaffoldBlocks(bot, mcData);

  bot.pathfinder.setMovements(movements);

  // プレイヤーの目の前の座標
  const pos = player.position.clone();
  const dx = Math.round(Math.cos(player.yaw));
  const dz = Math.round(Math.sin(player.yaw));
  const targetPos = pos.offset(dx, 0, dz);

  bot.chat(`${username} の所に行きます!`);
  bot.pathfinder.setGoal(new GoalBlock(targetPos.x, targetPos.y, targetPos.z));

  // 自動救出ループ
  let lastPos = bot.entity.position.clone();
  const checkInterval = setInterval(() => {
    const dist = bot.entity.position.distanceTo(targetPos);
    if (dist <= 1.5) {
      bot.chat(`到着しました!(距離 ${dist.toFixed(2)})`);
      clearInterval(checkInterval);
      return;
    }

    if (bot.entity.position.distanceTo(lastPos) < 0.01) {
      tryRescue(bot, targetPos);
    }
    lastPos = bot.entity.position.clone();
  }, 1500);
}

// ボットのインベントリーの中身を確認するコマンド
function inventoryCmd(bot, username) {
  const items = bot.inventory.items();
  if (items.length === 0) {
    bot.chat(`${username}、インベントリは空です。`);
    return;
  }
  const itemList = items
    .map((item) => `${item.count}x${item.name}(${item.type})`)
    .join(", ");
  bot.chat(`${username} の所持品: ${itemList}`);
}

// 四角形の家を建築するコマンド
async function buildEasyHouseCmd(bot, username) {
  const baseBlockPos = await buildBaseBlock(bot, "dirt");
  if (baseBlockPos) {
    await buildFloorFromBase(bot, baseBlockPos, "dirt");
    await moveBotToCenter(bot, baseBlockPos, 5);
    bot.chat("床を作りました!");
    await buildWallsFromBase(bot, baseBlockPos, "dirt", 5, 3);
    bot.chat("壁を作りました");
    await buildRoofFlatFromBase(bot, baseBlockPos, "dirt", 5, 3);
    bot.chat("屋根を作りました");
  }
}

// チャットから指定したボットへの命令を動的に実行する
function evalCmd(bot, username, code) {
  // VM2 サンドボックス作成
  const vm = new VM({
    timeout: 1000, // 無限ループ防止
    sandbox: {
      bot, // bot API を利用可能にする
      Vec3: require("vec3"), // 位置操作用
      console: { log: (...args) => bot.chat(args.join(" ")) }, // console.log をチャットに出力
    },
  });

  try {
    const result = vm.run(new String(code)); // コード実行
    if (result !== undefined) {
      bot.chat(`実行結果: ${result}`);
    }
  } catch (err) {
    bot.chat(`エラー: ${err.message}`);
  }
}

// AI に問い合わせてコードを生成してもらいそれを eval する
async function useAiCodeCmd(bot, username, prompt) {
  bot.chat(`AIにリクエスト中: "${prompt}"`);

  try {
    // 1. Ollama にリクエスト
    const response = await fetch("http://localhost:11434/api/generate", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        model: "gemma3", // 使うモデル名(Ollamaにあるもの)
        prompt: `
あなたはmineflayerのJavaScriptコードを書くアシスタントです。
ユーザーの指示に従い、mineflayer用のJavaScriptコードだけを出力してください。
bot オブジェクトは使えるので bot オブジェクトに対する操作をしてください。
説明文や余計な文章は書かず、コードのみを出力してください。
ユーザーの指示: ${prompt}
        `,
        stream: false,
      }),
    });

    const data = await response.json();
    let aiCode = data.response.trim();
    // Markdown のコードブロック記法を削除
    aiCode = aiCode
      .replace(/```(?:javascript|js)?\n?/gi, "")
      .replace(/```/g, "")
      .trim();

    console.log(aiCode);
    bot.chat("AIコードを受信、実行します…");

    // 2. サンドボックスで実行
    const vm = new VM({
      timeout: 2000,
      sandbox: {
        bot,
        Vec3: require("vec3"),
        console: { log: (...args) => bot.chat(args.join(" ")) },
      },
    });

    try {
      vm.run(aiCode);
      bot.chat("AIコードの実行が完了しました");
    } catch (err) {
      bot.chat(`コード実行エラー: ${err.message}`);
    }
  } catch (err) {
    bot.chat(`AI呼び出しエラー: ${err.message}`);
  }
}

module.exports = {
  moveCmd,
  comeOnCmd,
  inventoryCmd,
  buildEasyHouseCmd,
  evalCmd,
  useAiCodeCmd,
};

lib/command/parser.js

isUseAiCodeCommand を追加します

class CommandParser {
  constructor(message) {
    this.message = message.trim();
    this.parts = this.message.split(/\s+/);
  }

  isMoveCommand() {
    return this.parts[0].toLowerCase() === "move";
  }

  isComeOnCommand() {
    return this.parts[0].toLowerCase() === "comeon";
  }

  isInventoryCommand() {
    return this.parts[0].toLowerCase() === "inv";
  }

  isBuildEasyHouseCommand() {
    return this.parts[0].toLowerCase() === "build_easy_house";
  }

  isEvalCommand() {
    return this.parts[0].toLowerCase() === "eval";
  }

  isUseAiCodeCommand() {
    return this.parts[0].toLowerCase() === "use_ai_code";
  }

  getArgs() {
    return this.parts.slice(1);
  }
}

module.exports = CommandParser;

index.js

メインの処理を追記します
onChat の分岐処理を追記します

const mineflayer = require("mineflayer");
const { pathfinder, Movements } = require("mineflayer-pathfinder");
const CommandParser = require("./lib/command/parser");
const {
  moveCmd,
  comeOnCmd,
  inventoryCmd,
  buildEasyHouseCmd,
  evalCmd,
  useAiCodeCmd,
} = require("./lib/command/commands");

class MinecraftBot {
  constructor(host, port, username) {
    this.bot = mineflayer.createBot({
      host,
      port,
      username,
      version: false,
    });

    this.bot.loadPlugin(pathfinder);
    this.registerEvents();
  }

  registerEvents() {
    this.bot.once("spawn", () => this.onSpawn());
    this.bot.on("chat", (username, message) => this.onChat(username, message));
    this.bot.on("kicked", (reason) =>
      console.error("キックされました:", reason),
    );
    this.bot.on("error", (err) => console.error("エラー:", err));
    this.bot.on("end", () => console.log("Bot が切断されました"));
  }

  onSpawn() {
    console.log("Bot がログインしました!");
    this.bot.chat("こんにちは!Botクラスです!");

    const defaultMovements = new Movements(this.bot, this.bot.registry);
    this.bot.pathfinder.setMovements(defaultMovements);
  }

  onChat(username, message) {
    if (username === this.bot.username) return;
    console.log(`[${username}]: ${message}`);

    const parser = new CommandParser(message);

    if (parser.isMoveCommand()) {
      moveCmd(this.bot, username, parser.getArgs());
    } else if (parser.isComeOnCommand()) {
      comeOnCmd(this.bot, username);
    } else if (parser.isInventoryCommand()) {
      inventoryCmd(this.bot, username);
    } else if (parser.isBuildEasyHouseCommand()) {
      buildEasyHouseCmd(this.bot, username);
    } else if (parser.isEvalCommand()) {
      evalCmd(this.bot, username, parser.getArgs());
    } else if (parser.isUseAiCodeCommand()) {
      useAiCodeCmd(this.bot, username, parser.getArgs());
    }
  }
}

const myBot = new MinecraftBot("localhost", 25565, "bot");

動作確認

  • npx node index.js

チャットに「use_ai_code ハローとチャットして」と入力すると少し間を置いてからちゃんとチャットが表示されることが確認できます

最後に

Mineflayer と ollama を連携してボットを AI に動かしてもらうことに挑戦してみました

内部的には JavaScript を eval しているので eval で実行できないコードはそもそも実行できません

これ以上複雑なことをやろうとするならばやはりスクラッチですべて実装するかもしくは minecraft-mcp-server などを使うことになるかなと思います

2025年8月15日金曜日

Mineflayer で eval を使ってボットに動的にコードを実行させてみた

Mineflayer で eval を使ってボットに動的にコードを実行させてみた

概要

JavaScript には eval という文字列からコードを実行する仕組みがあります
今回はそれと Mineflayer を組み合わせてチャットに入力されたボット制御するコードを実行するような仕組みを作ってみました

注意点としては eval は非常に危険なコードなので使用する際には十分に注意してください
なお今回はサンドボックス化できる vm2 という仕組みを使って eval を使用します

環境

  • macOS 15.6
  • Minecraft 1.21.4
  • nodejs 22.15.1
  • mineflayer 4.31.0

vm2 のインストール

  • npm install vm2

lib/command/commands.js

vm2 を使って eval する処理を commands.js に追記します
サンドボックスなので VM オブジェクト生成時に渡したオブジェクトだけ eval 内で使えるようになります

const { VM } = require("vm2");
const { goals, Movements } = require("mineflayer-pathfinder");
const { GoalBlock } = goals;
const {
  buildBaseBlock,
  buildFloorFromBase,
  buildWallsFromBase,
  buildRoofFlatFromBase,
  moveBotToCenter,
} = require("./build/house");
const { tryRescue } = require("./dig/low_level_api");

const mcDataLoader = require("minecraft-data");

// 指定の座標に移動するコマンド
function moveCmd(bot, username, args) {
  if (args.length < 3) {
    bot.chat(`Usage: move <x> <y> <z>`);
    return;
  }

  const [x, y, z] = args.map(Number);
  if ([x, y, z].some(isNaN)) {
    bot.chat("座標は数値で指定してください");
    return;
  }

  bot.chat(`${username} の指示で (${x}, ${y}, ${z}) に移動します`);
  bot.pathfinder.setGoal(new GoalBlock(x, y, z));
}

// プレイヤーの目の前に移動するコマンド(canDig + 自動救出版)
function comeOnCmd(bot, username) {
  function getScaffoldBlocks(bot, mcData) {
    const usableItemIds = [
      mcData.itemsByName.dirt.id,
      mcData.itemsByName.cobblestone.id,
      mcData.itemsByName.sand.id,
    ];
    return bot.inventory
      .items()
      .filter((item) => usableItemIds.includes(item.type))
      .map((item) => item.type);
  }

  const player = bot.players[username]?.entity;
  if (!player) {
    bot.chat(`プレイヤー ${username} が見つかりません`);
    return;
  }

  const mcData = mcDataLoader(bot.version);
  const movements = new Movements(bot, mcData);

  // 各種設定
  movements.allowParkour = true;
  movements.allow1by1towers = true;
  movements.canDig = true;
  movements.scafoldingBlocks = getScaffoldBlocks(bot, mcData);

  bot.pathfinder.setMovements(movements);

  // プレイヤーの目の前の座標
  const pos = player.position.clone();
  const dx = Math.round(Math.cos(player.yaw));
  const dz = Math.round(Math.sin(player.yaw));
  const targetPos = pos.offset(dx, 0, dz);

  bot.chat(`${username} の所に行きます!`);
  bot.pathfinder.setGoal(new GoalBlock(targetPos.x, targetPos.y, targetPos.z));

  // 自動救出ループ
  let lastPos = bot.entity.position.clone();
  const checkInterval = setInterval(() => {
    const dist = bot.entity.position.distanceTo(targetPos);
    if (dist <= 1.5) {
      bot.chat(`到着しました!(距離 ${dist.toFixed(2)})`);
      clearInterval(checkInterval);
      return;
    }

    if (bot.entity.position.distanceTo(lastPos) < 0.01) {
      tryRescue(bot, targetPos);
    }
    lastPos = bot.entity.position.clone();
  }, 1500);
}

// ボットのインベントリーの中身を確認するコマンド
function inventoryCmd(bot, username) {
  const items = bot.inventory.items();
  if (items.length === 0) {
    bot.chat(`${username}、インベントリは空です。`);
    return;
  }
  const itemList = items
    .map((item) => `${item.count}x${item.name}(${item.type})`)
    .join(", ");
  bot.chat(`${username} の所持品: ${itemList}`);
}

// 四角形の家を建築するコマンド
async function buildEasyHouseCmd(bot, username) {
  const baseBlockPos = await buildBaseBlock(bot, "dirt");
  if (baseBlockPos) {
    await buildFloorFromBase(bot, baseBlockPos, "dirt");
    await moveBotToCenter(bot, baseBlockPos, 5);
    bot.chat("床を作りました!");
    await buildWallsFromBase(bot, baseBlockPos, "dirt", 5, 3);
    bot.chat("壁を作りました");
    await buildRoofFlatFromBase(bot, baseBlockPos, "dirt", 5, 3);
    bot.chat("屋根を作りました");
  }
}

// チャットから指定したボットへの命令を動的に実行する
function evalCmd(bot, username, code) {
  // VM2 サンドボックス作成
  const vm = new VM({
    timeout: 1000, // 無限ループ防止
    sandbox: {
      bot, // bot API を利用可能にする
      Vec3: require("vec3"), // 位置操作用
      console: { log: (...args) => bot.chat(args.join(" ")) }, // console.log をチャットに出力
    },
  });

  try {
    const result = vm.run(new String(code)); // コード実行
    if (result !== undefined) {
      bot.chat(`実行結果: ${result}`);
    }
  } catch (err) {
    bot.chat(`エラー: ${err.message}`);
  }
}

module.exports = {
  moveCmd,
  comeOnCmd,
  inventoryCmd,
  buildEasyHouseCmd,
  evalCmd,
};

lib/command/parser.js

isEvalCommand を追加します

class CommandParser {
  constructor(message) {
    this.message = message.trim();
    this.parts = this.message.split(/\s+/);
  }

  isMoveCommand() {
    return this.parts[0].toLowerCase() === "move";
  }

  isComeOnCommand() {
    return this.parts[0].toLowerCase() === "comeon";
  }

  isInventoryCommand() {
    return this.parts[0].toLowerCase() === "inv";
  }

  isBuildEasyHouseCommand() {
    return this.parts[0].toLowerCase() === "build_easy_house";
  }

  isEvalCommand() {
    return this.parts[0].toLowerCase() === "eval";
  }

  getArgs() {
    return this.parts.slice(1);
  }
}

module.exports = CommandParser;

index.js

メインの処理を追記します
onChat の分岐処理を追記します

const mineflayer = require("mineflayer");
const { pathfinder, Movements } = require("mineflayer-pathfinder");
const CommandParser = require("./lib/command/parser");
const {
  moveCmd,
  comeOnCmd,
  inventoryCmd,
  buildEasyHouseCmd,
  evalCmd,
} = require("./lib/command/commands");

class MinecraftBot {
  constructor(host, port, username) {
    this.bot = mineflayer.createBot({
      host,
      port,
      username,
      version: false,
    });

    this.bot.loadPlugin(pathfinder);
    this.registerEvents();
  }

  registerEvents() {
    this.bot.once("spawn", () => this.onSpawn());
    this.bot.on("chat", (username, message) => this.onChat(username, message));
    this.bot.on("kicked", (reason) =>
      console.error("キックされました:", reason),
    );
    this.bot.on("error", (err) => console.error("エラー:", err));
    this.bot.on("end", () => console.log("Bot が切断されました"));
  }

  onSpawn() {
    console.log("Bot がログインしました!");
    this.bot.chat("こんにちは!Botクラスです!");

    const defaultMovements = new Movements(this.bot, this.bot.registry);
    this.bot.pathfinder.setMovements(defaultMovements);
  }

  onChat(username, message) {
    if (username === this.bot.username) return;
    console.log(`[${username}]: ${message}`);

    const parser = new CommandParser(message);

    if (parser.isMoveCommand()) {
      moveCmd(this.bot, username, parser.getArgs());
    } else if (parser.isComeOnCommand()) {
      comeOnCmd(this.bot, username);
    } else if (parser.isInventoryCommand()) {
      inventoryCmd(this.bot, username);
    } else if (parser.isBuildEasyHouseCommand()) {
      buildEasyHouseCmd(this.bot, username);
    } else if (parser.isEvalCommand()) {
      evalCmd(this.bot, username, parser.getArgs());
    }
  }
}

const myBot = new MinecraftBot("localhost", 25565, "bot");

動作確認

  • npx node index.js

チャットを開いて eval bot.chat('hello'); と入力するとボットがチャットすることが確認できます
これで毎回チャットで別の命令をすることができます

最後に

eval を使ってボットに動的に命令する方法を実装してみました
VM2 でオブジェクトをサンドボックス化していますが bot オブジェクトはそのまま使えてしまうのでセキュリティには注意しましょう

また現状だと複雑なコードは実行不可なので単純な命令のみ実行できるレベルです

参考サイト