マイクラと連携してGUIDE01に表示してみた

こんにちは、牧長です。

GUIDE01で何ができるかな~っと考えていたのですが
子供と一緒にマイクラで遊んでいる時にふと思いました

マイクラの情報を補足情報として見れたら便利じゃないか?

技術選定

さて、やりたいことは決まったがどうやって取得したらいいのだろうか

調べてみるとRCONというのが使えそう

RCON(Remote Console)を使うと、サーバの外からコマンドを実行できるらしい

ざっくりと

  • 管理者専用の遠隔操作用コンソール
  • ゲーム内にログインしていない状態でもサーバコマンドが送れる
  • 主に自動化・運用・外部ツール連携に使われる

これを使って、こんな風にしようか

マイクラサーバの立ち上げ

まずはマイクラサーバを立ち上げないといけない

最近は便利ですね

そう、Dockerです

こんな感じのcompose.yamlで一撃で立ち上がりました

services:

  # =================================================== #
  #  Paper Server                                       #
  # =================================================== #
  paper:
    container_name: mc_paper
    image: itzg/minecraft-server
    tty: true
    stdin_open: true
    environment:
      ENABLE_ROLLING_LOGS: "TRUE"
      JVM_OPTS: "-XX:MaxRAMPercentage=75"
      TYPE: "PAPER"
      EULA: "TRUE"
      #VERSION: "LATEST"
      MOTD: "The World of Paper Server with Crossplay"
      MAX_PLAYERS: 5
      ENABLE_COMMAND_BLOCK: "TRUE"
      SNOOPER_ENABLED: "FALSE"
      VIEW_DISTANCE: 12
      PVP: "FALSE"
      LEVEL: "lv30066" # a default is "world"
      #ONLINE_MODE: "TRUE"
      ALLOW_FLIGHT: "TRUE"
      USE_NATIVE_TRANSPORT: "TRUE"
      STOP_SERVER_ANNOUNCE_DELAY: 60
      GUI: "FALSE"
      PLUGINS: |
      SERVER_PORT: "30066"
    # depends_on:
    #   - velocity
    ports:
      - "30066:30066/tcp"
      - "30066:30066/udp"
      - "25575:25575/tcp"
    volumes:
      - ./paper:/data
      - /etc/timezone:/etc/timezone:ro
    restart: unless-stopped

あとは下記のようにしたら立ち上がります

$ docker compose up -d

RCONで取得してみる

pythonスクリプトを書いてみます

# app.py
import os, re
from typing import List, Optional
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel
from mctools import RCONClient

from dotenv import load_dotenv; load_dotenv(".env")

RCON_HOST = os.getenv("RCON_HOST", "127.0.0.1")
RCON_PORT = int(os.getenv("RCON_PORT", "25575"))
RCON_PASSWORD = os.getenv("RCON_PASSWORD", "your-strong-password")

app = FastAPI(title="Minecraft Status API")

USERNAME_RE = re.compile(r"^[A-Za-z0-9_]{3,16}$")

def ticks_to_hhmm(t: int) -> str:
    total = (t + 6000) % 24000
    hour = total // 1000
    minute = int((total % 1000) * 60 / 1000)
    return f"{hour:02d}:{minute:02d}"

def parse_pos(resp: str):
    # "Pos: [x d, y d, z d]" or "[x d, y d, z d]"
    m = re.search(r"Pos:\s*\[([-\d\.Ee]+)d,\s*([-\d\.Ee]+)d,\s*([-\d\.Ee]+)d\]", resp)
    if not m:
        m = re.search(r"\[([-\d\.Ee]+)d,\s*([-\d\.Ee]+)d,\s*([-\d\.Ee]+)d\]", resp)
    if not m:
        raise ValueError(f"座標パース失敗: {resp}")
    return tuple(map(float, m.groups()))

def get_online_players(rcon: RCONClient) -> List[str]:
    # 1) list uuids(新形式)
    resp = rcon.command("list uuids")
    if "players online" in resp and ":" in resp:
        names = [part.split(" (",1)[0].strip() for part in resp.split(": ",1)[-1].split(",") if part.strip()]
        names = [n for n in names if USERNAME_RE.match(n)]
        if names:
            return names
    # 2) フォールバック: list(旧形式)
    resp = rcon.command("list")
    if "players online" in resp and ":" in resp:
        names = [n.strip() for n in resp.split(": ",1)[-1].split(",") if n.strip()]
        names = [n for n in names if USERNAME_RE.match(n)]
        return names
    return []

def query_status(target_player: Optional[str] = None):
    rcon = RCONClient(RCON_HOST, port=RCON_PORT, timeout=5.0)
    if not rcon.login(RCON_PASSWORD):
        raise HTTPException(status_code=503, detail="RCON認証に失敗しました")
    try:
        # プレイヤー確定
        player = (target_player or "").strip()
        if player:
            if not USERNAME_RE.match(player):
                raise HTTPException(status_code=400, detail=f"不正なプレイヤー名: {player}")
        else:
            online = get_online_players(rcon)
            if not online:
                raise HTTPException(status_code=404, detail="オンラインのプレイヤーがいません")
            player = online[0]

        # ここは execute as ではなく data get を直接使う
        pos_resp = rcon.command(f"data get entity {player} Pos")
        x, y, z = parse_pos(pos_resp)

        time_resp = rcon.command("execute in minecraft:overworld run time query daytime")
        m = re.search(r"The time is (\d+)", time_resp)
        if not m:
            raise HTTPException(status_code=502, detail=f"時刻取得に失敗: {time_resp}")
        daytime = int(m.group(1))

        return {
            "player": player,
            "position": {"x": x, "y": y, "z": z},
            "time": {"daytime_ticks": daytime, "clock_hhmm": ticks_to_hhmm(daytime)},
        }
    finally:
        try: rcon.stop()
        except: pass

# ── Schemas
class Position(BaseModel):
    x: float; y: float; z: float
class TimeInfo(BaseModel):
    daytime_ticks: int; clock_hhmm: str
class StatusResponse(BaseModel):
    player: str
    position: Position
    time: TimeInfo

@app.get("/healthz")
def healthz():
    return {"ok": True}

@app.get("/players")
def players():
    rcon = RCONClient(RCON_HOST, port=RCON_PORT, timeout=5.0)
    if not rcon.login(RCON_PASSWORD):
        raise HTTPException(status_code=503, detail="RCON認証に失敗しました")
    try:
        return {"online": get_online_players(rcon)}
    finally:
        try: rcon.stop()
        except: pass

@app.get("/status", response_model=StatusResponse)
def status(player: Optional[str] = Query(None, description="対象プレイヤー名。未指定ならオンラインから自動選択")):
    return query_status(player)

簡単に概要

  • FastAPIでHTTPサーバ待ち受け
  • RCONでプレイヤーの座標位置とゲーム内時刻を取得
  • /statusでjson形式でレスポンス

このスクリプトはvenvを使ってますのでまず初めにこんな感じで仮想環境作っておきましょう

sudo apt install -y python3-venv
python3 -m venv .venv
source .venv/bin/activate

あとはpipでライブラリのインストール

python -m pip install --upgrade pip
pip install mcpi mcrcon fastapi uvicorn mctools dotenv

これで準備ができたので下記のようにコマンドを叩いてサーバを立ち上げましょう

uvicorn app:app --host 0.0.0.0 --port 8080 --env-file .env

これでアクセスすると

{"detail":"オンラインのプレイヤーがいません"}

ゲーム内にログインした状態でアクセスすると

{
  "player": "MackyRocky",
  "position": {
    "x": -8.672398768830408,
    "y": 67.0,
    "z": 62.533052623416964
  },
  "time": {
    "daytime_ticks": 13288,
    "clock_hhmm": "19:17"
  }
}

GUIDE01への表示

あとはスマホアプリにGUIDE SDKを組み込んで、上記APIから取得した情報を画面上に表示するとこうなります

最後に

GUIDE01はシンプルなデバイスですが、だからこそアイデア次第で様々なことができるデバイスです

こんなことできたら面白いのでは?みたいなみんなのアイデアで一緒にデバイスを作り上げていきたいです

コメントを送信