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 オブジェクトはそのまま使えてしまうのでセキュリティには注意しましょう

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

参考サイト

0 件のコメント:

コメントを投稿