【CC:Tweaked】MinecraftにてBotのデプロイに成功した話
はじめに
この記事は、Minecraft内でLua言語のプログラムを動かせるMOD(ゲーム可変プログラム)、『Computer Craft(CC: Tweaked)』を活用し、コミュニケーションツール『Discord』のBotを動作させることを目的とした記事です。
使用環境
Minecraft Forge 1.20.1
CC:Tweaked 1.113.1
結論
CC:Tweaked環境でのDiscord bot実装において、WebSocket制限は根本的な技術的制約であることが判明したため、REST APIベースの実装によりこの制約を完全に回避し安定したbot機能を実現しました。
REST APIの実装
HTTP REST API ベースの設計
-- HTTPリクエストヘルパー関数
local function discord_request(endpoint, method, data)
local url = "https://discord.com/api/v10" .. endpoint
local headers = {
["Authorization"] = "Bot " .. config.token,
["Content-Type"] = "application/json",
["User-Agent"] = "DiscordBot (CC:Tweaked, 1.0)"
}
local handle, err
if method == "GET" then
handle, err = http.get(url, headers)
elseif method == "POST" then
handle, err = http.post(url, options.body, headers)
end
if handle then
local response_body = handle.readAll()
handle.close()
return json(response_body), nil
else
return nil, err
end
end
ポーリングベースのメッセージ処理
-- メッセージポーリングループ
local function message_polling_loop(callback)
while true do
if channel_id then
local messages = get_messages(channel_id, 5)
if messages and #messages > 0 then
for i = #messages, 1, -1 do
local message = messages[i]
if message.id ~= last_message_id then
if callback then
callback(message)
end
last_message_id = message.id
end
end
end
end
os.sleep(2)
end
end
CC:Tweakedについて
ここからは、ComputerCraft(CC:Tweaked)について解説していきます
1.ComputerCraftとは
ComputerCraftは、dan200氏が開発したMinecraftのMODであり、Minecraftの世界に、プログラムの作成・実行が可能なコンピュータや、プログラムで動作させることができるロボットを追加するMODです。
Minecraftでは、ゲーム内に登場する「レッドストーン」というアイテムが存在し、仮想のデジタル回路を構築することができます。疑似的な導線や入力装置、出力装置を組み合わせ、論理ゲートを構築することができ、複雑な回路を構築することができますが、本MODではプログラムによってそれらを再現することができます。

Minecraftにて再現したXOR回路
ComputerCraftは、Minecraft 1.12(2017年リリースのアップデート)以降のバージョン更新を停止し、2017年にGitHubにてオープンソース化されました。その後、有志の開発者により様々な派生が作成されましたが、SquidDev氏の開発した「CC: Tweaked」を今回は採用しました。

CC:Tweakedで実装されているコンピューター
2.ディレクトリ構造
CC:Tweakedは、Javaで動作するMinecraft内でLuaを動作させるため、「Craft OS」という独自OSがゲーム内のコンピューターに組み込まれています。Craft OSのディレクトリはゲーム内のセーブファイルに保存されており、以下のような構造となっています。
minecraft/
└── [セーブデータ名]
├── (省略)
└── saves
├── (省略)
└── computercraft/
├── computer/
| └── [ゲーム内コンピューターの識別番号]
| └── [Luaの実行ファイル]
└── ids.json
余談ですが、Luaの実行ファイルは、一応ゲーム内のコンピューターを用いて編集が可能ですが、他IDE・コードエディタと比較して機能が少ないため、VSCode等のツールでファイルを直接編集することをおすすめします。

ゲーム内コンピューターに搭載されているエディタ
実装の過程
ここからは、CC:Tweakedを用いたDiscord Bot実装の過程について記述していきます。
1.WebSocketを用いたBot構築
CC:Tweakedに標準で搭載されているhttp通信を利用しました。
なお、参考資料が少なかったため、一部にAI(Gemini 2.5Pro)を使用しています。
Discord Developer Portalの設定については、下記記事を参照しました。
また、プログラムの整理のためディレクトリ構成を試験的に以下のようにしています。
└── [ゲーム内コンピューターの識別番号]
└── config.lua ー ボットトークン、チャンネル、その他設定等
└── discord_lib.lua ー Discordとの通信を行うコア機能
└── startup.lua ー ボットの起動
config.lua
local config = {}
config.token = "YOUR_BOT_TOKEN"
config.identity = {
op = 2, -- オペコードの指定
d = {
token = config.token,
intents = 3328, -- Discordの権限(GUILDS (1) + GUILD_MESSAGES (512) + MESSAGE_CONTENT (32768) = 33281)
properties = {
os = "CraftOS",
browser = "CC:Tweaked",
device = "Computer"
}
}
}
return config
discord_lib.lua
local json = textutils.unserializeJSON
local serialize = textutils.serializeJSON
local M = {}
local ws
local heartbeat_interval
local last_sequence = nil
-- ハートビートを送信し続ける関数
local function send_heartbeat()
while true do
os.sleep(heartbeat_interval / 1000)
local payload = { op = 1, d = last_sequence }
local ok, err = ws.send(serialize(payload))
if ok then
print("Heartbeat sent.")
else
printError("Heartbeat failed: " .. tostring(err))
break
end
end
end
-- イベントを受信し続ける関数
local function event_listener(callback)
while true do
local msg, isBinary = ws.receive()
if not msg then
printError("Connection closed.")
break
end
local data = json(msg)
if data.s then
last_sequence = data.s
end
-- Opcode 0 (Dispatch) イベントのみを処理
if data.op == 0 then
if data.t == "MESSAGE_CREATE" then
callback(data.d)
end
end
end
end
-- メインの接続関数
function M.connect(config, callback)
-- 1. Gateway URLの取得
print("Getting Gateway URL...")
local handle, err = http.get("https://discord.com/api/v10/gateway/bot", {
["Authorization"] = "Bot " .. config.token
})
if not handle then printError("Failed to get gateway URL: " .. err); return end
local body = handle.readAll()
handle.close()
local gateway_url = json(body).url .. "/?v=10&encoding=json"
-- WebSocket接続
print("Connecting to Gateway: " .. gateway_url)
ws, err = http.websocket(gateway_url)
if not ws then printError("WebSocket connection failed: " .. err); return end
print("WebSocket Connected!")
-- 最初に必ず届く Opcode 10 Hello を受信する
print("Waiting for Hello from Discord...")
local first_msg = ws.receive()
if not first_msg then printError("Connection failed before Hello"); ws.close(); return end
local hello_data = json(first_msg)
if hello_data.op ~= 10 then
printError("Expected Opcode 10 (Hello), but got " .. tostring(hello_data.op))
ws.close()
return
end
print("Received Hello from Discord.")
heartbeat_interval = hello_data.d.heartbeat_interval
-- Helloを受信後、速やかにIdentifyペイロードを送信する
print("Sending Identify payload...")
ws.send(serialize(config.identity))
-- これで準備完了。ハートビートとイベントリスナーを並行で開始する
print("Bot Identified. Starting main loops...")
parallel.waitForAny(
function() send_heartbeat() end,
function() event_listener(callback) end
)
-- どちらかの処理が終わったら(=接続が切れたら)接続を閉じる
ws.close()
end
return M
startup.lua
local config = require("config")
local discord = require("discord_lib")
term.clear()
term.setCursorPos(1, 1)
print("Discord Bot Initializing...")
-- Discordからメッセージが届いた時に実行する処理を定義
local function on_message_received(message)
local author = message.author.username
local content = message.content
print(author .. ": " .. content)
end
-- ライブラリのconnect関数を呼び出してボットを開始
-- on_message_received関数を渡して、メッセージ受信時に実行されるようにする
discord.connect(config, on_message_received)
print("Bot has been disconnected.")
実装内容
本実装によって、DiscordBotをアクティブにし、チャットしたユーザー名とそのユーザーのチャット内容の読み取りに成功
課題
ボットは正常に動作するが、約40秒程度経過すると必ず接続が切断される
2.ハートビート(死活監視)の対応
上記コードの原因として、Discord側が要求するハートビート(死活管理)の対応に失敗することが挙げられました。本問題の原因と思われる点に以下が挙げられました。
- 処理の遅延によって要求するハートビートの送信タイミングに信号を送れない可能性
- CC:Tweakedの
parallel関数のバグ- CC:Tweakedのコンピューターに割り当てられるメモリが不足している
- CC:TweakedのWebSocket通信に制限がある
2.1.待機方式からタイマー方式に変更
os.sleep()関数を使用し待機後にハートビートを送信する方式では、処理による遅延によってDiscordが期待するタイミングからわずかに遅れてしまっているのが原因と考えられました。
これを解決するため、より安定的で信頼性の高いタイマー方式にプログラム全体を書き直しました。
-- discord_lib.lua
local json = textutils.unserializeJSON
local serialize = textutils.serializeJSON
local M = {}
local ws
local heartbeat_timer = nil
local heartbeat_interval
local last_sequence = nil
local received_heartbeat_ack = true
local function send_heartbeat()
if not received_heartbeat_ack then
printError("Heartbeat ACK not received. Connection may be unstable.")
ws.close()
return false
end
received_heartbeat_ack = false
local payload = { op = 1, d = last_sequence }
local ok, err = ws.send(serialize(payload))
if ok then
print("Heartbeat sent.")
else
printError("Heartbeat failed to send: " .. tostring(err))
return false
end
return true
end
function M.connect(config, callback)
print("Getting Gateway URL...")
local handle, err = http.get("https://discord.com/api/v10/gateway/bot", {
["Authorization"] = "Bot " .. config.token
})
if not handle then printError("Failed to get gateway URL: " .. err); return end
local body = handle.readAll()
handle.close()
local gateway_url = json(body).url .. "/?v=10&encoding=json"
print("Connecting to Gateway: " .. gateway_url)
ws, err = http.websocket(gateway_url)
if not ws then printError("WebSocket connection failed: " .. err); return end
print("WebSocket Connected!")
while true do
local event, p1, p2 = os.pullEvent()
print("DEBUG: Event pulled:", event)
if event == "websocket_message" then
if p1 ~= gateway_url then goto continue end
local data = json(p2)
if data.s then last_sequence = data.s end
local opcode = data.op
if opcode == 10 then
print("Received Hello from Discord.")
heartbeat_interval = data.d.heartbeat_interval
print("Sending Identify payload...")
ws.send(serialize(config.identity))
local first_delay = heartbeat_interval * math.random() / 1000
heartbeat_timer = os.startTimer(first_delay)
print("Bot Identified. First heartbeat in " .. string.format("%.2f", first_delay) .. "s.")
elseif opcode == 11 then
received_heartbeat_ack = true
elseif opcode == 0 then
if data.t == "MESSAGE_CREATE" then
callback(data.d)
end
end
elseif event == "timer" then
if p1 == heartbeat_timer then
if send_heartbeat() then
heartbeat_timer = os.startTimer(heartbeat_interval / 1000)
else
break
end
end
elseif event == "websocket_closed" then
printError("WebSocket connection was closed.")
break
end
::continue::
end
if heartbeat_timer then os.cancelTimer(heartbeat_timer) end
if ws then ws.close() end
print("Bot has been disconnected.")
end
return M
結果
**ハートビートに失敗。**40秒程度で切断した。
原因
原因が不明だったので、AI(Gemini 2.5Pro)を使用して解読しました。
これは非常に奇妙で、解決が難しい問題です。
ログを詳しく見ると、以下のことがわかります。
ボットは正常に認証され、Discordもそれを受け入れています。
「30.74秒後に最初のハートビートを送ります」というタイマーの予約にも成功しています。
しかし、実際にタイマーが作動してハートビートを送ろうとした瞬間には、すでに接続が切れてしまっています。(Heartbeat failed to send: nil は、ws.send を試みたが接続が存在しなかった、という意味です)
つまり、タイマーが作動するまでの30秒間の待機時間中に、何らかの原因で接続が「静かに」切断されていると考えられます。本来であればwebsocket_closedというイベントが記録されるはずですが、それも表示されていません。
これは、前回導入した「os.pullEventを使った単一のイベントループ」という仕組みが、ComputerCraftの環境と稀な相性問題を起こし、WebSocket接続を意図せず不安定にさせている可能性を示唆しています。
以上から、os.pullEvent方式を廃止し、parallel.waitForAny方式に修正を試みました。
2.2.parallel関数への修正
-- discord_lib.lua
local json = textutils.unserializeJSON
local serialize = textutils.serializeJSON
local M = {}
local ws
local heartbeat_interval
local last_sequence = nil
local function heartbeater()
local first_delay = heartbeat_interval * math.random() / 1000
os.sleep(first_delay)
local payload = { op = 1, d = last_sequence }
local ok, err = ws.send(serialize(payload))
if not ok then
printError("Failed to send first heartbeat: " .. tostring(err))
return
end
print(" First heartbeat sent.")
while true do
os.sleep(heartbeat_interval / 1000)
payload = { op = 1, d = last_sequence }
ok, err = ws.send(serialize(payload))
if ok then
print("Heartbeat sent.")
else
printError("Failed to send subsequent heartbeat: " .. tostring(err))
break
end
end
end
local function event_listener(callback)
while true do
local msg = ws.receive()
if not msg then
printError("Connection lost in event listener.")
break
end
local data = json(msg)
print("DEBUG: Received Opcode " .. tostring(data.op) .. ", Event Type: " .. tostring(data.t))
if data.s then
last_sequence = data.s
end
if data.op == 0 and data.t == "MESSAGE_CREATE" then
callback(data.d)
end
end
end
function M.connect(config, callback)
print("Getting Gateway URL...")
local handle, err = http.get("https://discord.com/api/v10/gateway/bot", {
["Authorization"] = "Bot " .. config.token
})
if not handle then printError("Failed to get gateway URL: " .. err); return end
local gateway_url = json(handle.readAll()).url .. "/?v=10&encoding=json"
handle.close()
print("Connecting to Gateway: " .. gateway_url)
ws, err = http.websocket(gateway_url)
if not ws then printError("WebSocket connection failed: " .. err); return end
print("WebSocket Connected!")
local hello_msg = ws.receive()
if not hello_msg then printError("Did not receive Hello from Discord."); ws.close(); return end
local hello_data = json(hello_msg)
heartbeat_interval = hello_data.d.heartbeat_interval
print("Received Hello. Heartbeat interval: " .. heartbeat_interval .. "ms")
print("Sending Identify payload...")
ws.send(serialize(config.identity))
print("Bot Identified. Starting parallel loops...")
parallel.waitForAny(
function() heartbeater() end,
function() event_listener(callback) end
)
if ws then ws.close() end
print("Bot has been disconnected.")
end
return M
結果
失敗。ボットが認証を終えた直後(10秒〜30秒)に、ログなしで接続が切れるようになった。
原因
原因が不明だったので、AI(Gemini 2.5Pro)を使用して解読しました。
度重なる試行、誠にありがとうございます。そして、問題が解決せず大変申し訳ございません。
ログを拝見し、ユーザー様からの「10秒程度で切れる」というご報告と組み合わせた結果、これまでの仮説がすべて覆る、たった一つの根本原因にたどり着くことができました。
真の根本原因:コンピューターの性能限界
問題は、プログラムのコードではなく、ボットを動かしているコンピューターそのものにあります。
このエラーは、ほぼ間違いなく普通のアドバンスドではないコンピューター (Normal Computer) でプログラムを実行しているために発生しています。
何が起きているか:
ボットがDiscordに接続すると、Discordは「サーバーの全情報(READYイベント)」という巨大な初期データを送り返します。
普通のコンピューターのメモリ(RAM)は512KBしかありません。
この巨大なデータをws.receive()で受信しようとした瞬間、512KBのメモリがすべて使い果たされ、コンピューター自体がクラッシュしています。
これが、エラーログも出さずにプログラムが突然停止し、接続が「静かに」切れているように見えた真の理由です。メモリ不足でOSごと落ちてしまっているため、デバッグログを表示する余裕すらないのです。
以上から、CC:Tweakedの個別のコンピューターに割り当てられるメモリー数を、config設定から変更することにしました。
2.3.CC:Tweakedのコンピューターの割り当てメモリ数を増加
CC:Tweakedのconfig設定ファイルは以下のディレクトリにあります。
minecraft/
└── [セーブデータ名]
├── (省略)
└── saves
├── (省略)
└── serverconfig/
├── forge-server.toml
└── computercraft-server.toml ー 今回変更するファイル
computercraft-server.tomlファイルでは、コンピューター一台に割り当てられるストレージ量やHTTP APIの制限、無線通信の範囲などを設定できます。
以下の記事を参照して設定の変更を試みました。
ファイル内にメモリー数を割り当てる項目がありませんでした。
<補足>
CC:Tweakedにはメモリー数の上限が存在しません。
CC:TweakedのコンピュータはMinecraftサーバーまたはクライアントを実行する基盤となるJava仮想マシン(JVM)が提供する、動的で共有されたメモリプール内で動作します。
Geminiの回答の中でコンピュータ、及びアドバンスドコンピューターのメモリ割り当て数についての言及がありましたが、これらはすべてハルシネーションと考えられます。
2.4.WebSocket通信以外の方法を模索
コンフィグの参照により、CC:TweakedのHTTP APIには制約があることがわかりました。
そのため、WebSocket通信を利用したDiscord通信には問題がある可能性があります。
確認のため、以下のテストを実施しました。
<Identify ペイロード送信のテスト>
-- 送信失敗
local ok, err = ws.send(identify_payload)
-- 結果: ok = nil, err = nil
<WebSocket 送信のテスト>
-- テスト送信も失敗
local test_ok = ws.send('{"test":true}')
-- 結果: test_ok = nil (送信機能が完全に無効化)
これらのテストにより、以下の原因が判明しました。
- 送信機能の喪失: Hello メッセージ受信後、WebSocket が読み取り専用状態になる
- Discord Gateway との互換性問題: CC:Tweaked の WebSocket 実装が Discord Gateway の仕様と完全に互換していない
以上から、WebSocket通信に制限があるため、回避のためにDisocord REST APIを使用した実装に変更しました。
-- HTTPリクエストヘルパー関数
local function discord_request(endpoint, method, data)
local url = "https://discord.com/api/v10" .. endpoint
local headers = {
["Authorization"] = "Bot " .. config.token,
["Content-Type"] = "application/json",
["User-Agent"] = "DiscordBot (CC:Tweaked, 1.0)"
}
local handle, err
if method == "GET" then
handle, err = http.get(url, headers)
elseif method == "POST" then
handle, err = http.post(url, options.body, headers)
end
if handle then
local response_body = handle.readAll()
handle.close()
return json(response_body), nil
else
return nil, err
end
end
-- メッセージポーリングループ
local function message_polling_loop(callback)
while true do
if channel_id then
local messages = get_messages(channel_id, 5)
if messages and #messages > 0 then
for i = #messages, 1, -1 do
local message = messages[i]
if message.id ~= last_message_id then
if callback then
callback(message)
end
last_message_id = message.id
end
end
end
end
os.sleep(2) -- 2秒間隔でポーリング
end
end
終わりに
CC:Tweaked環境でのDiscord bot実装において、WebSocketにはサーバーの安定化のために、config設定にて一定の制限がされていることが判明しました。
REST APIベースの実装により、この制約を完全に回避し、安定したbot機能を実現することができます。
今回の実装では、Discordのログインとテキストの読み取りという簡易的で一方的な通信を実装しましたが、今後はスラッシュコマンドの実行等のDiscord botの様々な機能を備えたライブラリの構築を目指していきます。
関連項目
GitHubレポジトリ: https://github.com/HikariTakahashi/MineDisco
Discussion