Factorio ヘッドレスサーバーのマップを Mapshot で公開する

技術系

自宅 WSL2 で動かしている Factorio (Space Age) のヘッドレスサーバーについて、「今のマップを Web ブラウザでぐりぐり眺められたら楽しいよね」という思いつきから、定期的にマップ画像を生成して公開する仕組みを構築しました。最終的には Mapshot という Factorio mod + 外部 CLI ツール + Cloudflare Tunnel の組み合わせで、https://map1.welovefactorio.com から誰でも閲覧できるようになっています。

Mapshot で生成された Nauvis のマップ
完成形:Leaflet ベースで地図をズーム/パンできる

目次

何を作ったのか

cron が 5 分おきにスクリプトを叩き、ゲーム内時間が前回から 10 分以上進んでいたら新しいマップ画像を生成、Web で公開する。誰もサーバーに接続していない時は auto_pause で tick が進まないので、無駄なレンダリングは走らない。生成されたマップ画像は履歴として蓄積され、過去の任意の時点に戻ってマップを見ることもできます。

システム構成図
システム全体の構成。ヘッドレスサーバーは触らず、別途グラフィカル版 Factorio を立ててレンダリングだけさせる。

ツール選定:FactorioMaps か Mapshot か

定番は 2 つあります。

  • FactorioMaps (L0laapk3/FactorioMaps): 歴史が長く、タイムラプス機能あり。ただし主に Factorio 1.1 向けで、2.0 + Space Age への対応は fork ベースで実験段階。
  • Mapshot (Palats/mapshot): 比較的新しい。最新版 0.0.28 (5 ヶ月前) で公式に Factorio 2.0 をサポート。Linux ヘッドレスサーバー運用が想定されており、CLI が用意されている。

Space Age が前提だったので Mapshot を選択。後述しますが、これは正解でした。

Mapshot のしくみ

Mapshot は 3 つの部品で構成されています。

  1. Lua mod: ゲーム内で game.take_screenshot() API を呼んでマップをタイル画像化する本体。
  2. Go 製 CLI: Factorio 本体をバックグラウンドで起動し、mod を仕込み、完了を検知して Factorio を終了させる司令塔。
  3. SPA フロントエンド: 生成されたタイル画像を Leaflet.js で表示する HTML/JS 一式。

Mapshot は 「マップ画像生成」と「配信」を分離した設計になっており、出力は完全に静的な HTML/JS/JPG です。任意の HTTP サーバーで配信できます (mapshot 自身に HTTP サーバー機能もあり)。

生成された個別タイル画像
1 タイルあたり 512×512 の JPG。これが多数集まって 1 枚のマップになる。

詰まりポイント (覚え書き)

1. ヘッドレスサーバーでは描画できない

当たり前といえば当たり前ですが、ヘッドレス版 Factorio は描画機能を持っていません。Mapshot のレンダリングは game.take_screenshot() という engine API を使うため、グラフィカル版 Factorio が必要です。WSL2 で X サーバーが無いので、xvfb-run で仮想ディスプレイを与えて起動します。

稼働中のサーバーには触らず、別途グラフィカル版を入れる構成にしました。レンダリング時は最新セーブ (saves/world.zip) を一時ディレクトリにコピーして、それをグラフィカル版が読み込みます。

2. 「Mod elevated-rails requires Space Age expansion」エラー

これが一番ハマりました。Factorio の anonymous DL ページから取れる「alpha」ビルドは Space Age を含まない「素のグラフィカル版」で、DLC 機能フラグ (rail-bridges, quality, freezing 等) が無効。elevated-rails mod を有効化するとこの DLC フラグが必要なため、起動が拒否されます。

解決策は 「expansion」ビルドをダウンロードすること。URL は次のような形式:

https://www.factorio.com/get-download/2.0.76/expansion/linux64?username=<u>&token=<token>

サイズは alpha が 1.4GB に対し expansion は 4.2GB。期待通り Space Age 一式 (Vulcanus, Fulgora, Gleba, Aquilo の各惑星アセット含む) が入っています。なお username と service-token は ~/factorio-server/server-settings.json に既に記載されているもの (普段ヘッドレスサーバーが認証に使っているもの) を流用できます。

3. script-output のパスの食い違い

これは小さいけど厄介でした。Mapshot CLI は --factorio_datadir/script-output/ を見て「レンダリング完了」を検知しますが、Factorio 本体は config.iniwrite-data パス配下に script-output/ を作ります。デフォルト構成だと 2 つは別ディレクトリになり、CLI は永遠に完了を検知できず、ハング状態になります。

解決策は --factorio_scriptoutput オプションを明示的に指定すること。CLI と Factorio 本体で同じパスを見るように揃えます。

xvfb-run -a ./bin/mapshot render \
  --factorio_binary ./factorio/bin/x64/factorio \
  --factorio_datadir ./factorio/data \
  --factorio_scriptoutput ./factorio/script-output \
  --work_dir ./work \
  --area all \
  --surface _all_ \
  --minjpgquality 50 \
  ./tmp/snapshot.zip

4. マップが「半分切れたように」見える

初期設定だと「プレイヤー建造物のあるチャンクのみ」が描画され、未開拓の地形タイルは省略されます。これだと序盤は本拠地のごく狭い範囲しか見えず、残りが黒く欠けたようになります。

解決策は --area all--minjpgquality 50 の指定。前者で「生成済みの全チャンク」を対象にし、後者で「エンティティが無いタイルでも JPG を出力する」よう強制します。これでタイル数が一気に増え (例: zoom_4 で 3 → 182 タイル)、全域が見えるようになりました。

cron + ゲーム内時間ゲート

cron は単純に 5 分おきに発火させます。ただし毎回レンダリングを走らせると無駄なので、稼働中のサーバーから RCON で game.tick を取得し、前回の tick との差が「ゲーム内 10 分 (= 60 tick/秒 × 600 秒 = 36000 tick)」以上の時だけ実行するようにしました。

# RCON で game.tick を取得 (Source RCON プロトコルを 30 行 Python で自前実装)
CUR_TICK=$(python3 ./rcon.py 127.0.0.1 27015 "$RCON_PW" \
    "/silent-command rcon.print(tostring(game.tick))" | tr -d '[:space:]')

LAST_TICK=$(cat .last-tick 2>/dev/null || echo 0)
DIFF=$((CUR_TICK - LAST_TICK))
[ "$DIFF" -lt 36000 ] && exit 0  # 10 ゲーム分未満ならスキップ

サーバー設定が auto_pause: true なので、誰もログインしていない夜間は tick が進まず、自動的にレンダリングも走らない。地味に嬉しい挙動です。

Cloudflare Tunnel での公開

もともと別用途 (glitchtip) で使っていた Cloudflare Tunnel に ingress を追加するだけ。welovefactorio.com は別ドメインだったため、cloudflared tunnel login を再実行してこのドメインの zone を許可する cert.pem を別ファイルとして用意しました。

# cert を zone 別に分けて管理
mv ~/.cloudflared/cert.pem ~/.cloudflared/cert-l4tp.pem
cloudflared tunnel login    # ブラウザで welovefactorio.com を選択
mv ~/.cloudflared/cert.pem ~/.cloudflared/cert-welovefactorio.pem

# DNS route は --origincert で使い分け
cloudflared tunnel \
  --origincert ~/.cloudflared/cert-welovefactorio.pem \
  route dns <tunnel-id> map1.welovefactorio.com

そして ~/.cloudflared/config.yml に新 ingress を追記して cloudflared を再起動。同じ tunnel daemon で 2 ドメインを捌けます。

履歴機能:mapshot serve への切り替え

動作確認の段階では python3 -m http.server で「最新の 1 レンダリングだけ」を配信していました。ところが Mapshot は毎回のレンダリング結果を別ディレクトリ d-<hash>/ として堆積する設計になっており、過去の出力は実は全部残っています。

そこで配信を mapshot serve に切り替えました。これは script-output/ 配下を SPA で公開するモードで、複数のレンダリングを一覧化して切り替えられる UI が標準で付いてきます。

mapshot serve の履歴一覧 UI
累積したレンダリングが時系列で並ぶ。「latest」と「28s ago」のように相対時刻表示。

古い履歴は generate-map.sh 末尾でローテーションさせています。設定では「直近 10000 件まで」としているので、1 日 24 時間 144 件レンダリングしても 70 日弱は遡れる計算。容量上限は ~30GB です。

# レンダリング後に古い d-* を削除
for sdir in "$SCRIPT_OUTPUT/mapshot"/*/; do
    ls -1dt "$sdir"d-*/ 2>/dev/null | tail -n +$((RETAIN_COUNT + 1)) | xargs -r rm -rf
done

mapshot serve は systemd で常駐

[Unit]
Description=Factorio Mapshot HTTP server
After=network.target

[Service]
Type=simple
User=yousan
WorkingDirectory=/home/yousan/games/factorio1/maps
ExecStart=/home/yousan/games/factorio1/maps/bin/mapshot serve --port 8765 \
    --factorio_scriptoutput /home/yousan/games/factorio1/maps/factorio/script-output
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

完成形のディレクトリ構成

/home/yousan/games/factorio1/
├── server/                        # ヘッドレスサーバー (本番)
│   ├── bin/x64/factorio
│   └── ...
├── saves/world.zip                # 共有セーブ
├── config/rconpw                  # RCON パスワード
└── maps/                          # マップ生成・配信用
    ├── factorio/                  # グラフィカル版 (expansion ビルド)
    │   └── script-output/mapshot/snapshot/
    │       ├── d-<hash1>/         # 履歴 (古い)
    │       ├── d-<hash2>/
    │       └── d-<latest>/        # 最新
    ├── bin/mapshot                # CLI バイナリ (Palats/mapshot 0.0.28)
    ├── work/                      # mapshot 作業ディレクトリ
    ├── tmp/snapshot.zip           # レンダリング用一時セーブ
    ├── generate-map.sh            # cron エントリポイント
    ├── rcon.py                    # RCON クライアント
    ├── render.log
    └── .last-tick                 # 前回レンダリング時の game.tick

所感

  • Mapshot の出力が「完全に静的」なので、配信は何でもアリ。Cloudflare Pages にデプロイしても、GitHub Pages に push しても良い。今回はトラフィックが少ないので systemd で常駐させた mapshot serve で十分。
  • 「ゲーム内時間ゲート」は地味に効く。auto_pause と組み合わせると、誰も遊んでいない時間帯は完全に走らないので CPU・ディスクともにゼロコスト。
  • 履歴が「ハッシュ付きディレクトリで自動的に堆積する」設計は、後から振り返るのに本当に便利。タイムラプス動画化したくなったら ffmpeg で zoom_0 のタイル群を時系列に連結すれば作れそう。
  • Space Age expansion build を anonymous URL でダウンロードできる (Factorio アカウントの service-token があれば) ことを今回知った。便利。

参考

コメント

タイトルとURLをコピーしました