Simutrans OTRPのHeadlessサーバーを建てるのに苦労した話

# TL; DR
- `singleuser_install`を0にしよう
- 新規と2回目以降で読み込むべきファイルは異なることに注意

# 経緯
？？？「Simutransをみんなでワイワイやりたい」
？？？「でもうちに外からアクセスできるIPアドレスなんてない」

「「そうだ、借りているVPSにサーバーを載せてしまえばいいんだ」」

# Simutransについて
Simutransは、ブラジル発の鉄道シミュレーションゲームです。多言語対応・マルチプラットフォーム・画像や性能などを簡単にカスタマイズ可能…とあり、現在も多くの方が愛用しています（要出典）。
ゲーム全体が**オープンソース**であり、プログラミングの知識があれば、誰でも挙動をカスタマイズすることができます。
Simutransには、pakと呼ばれるアドオンシステムがあり、車両や線路、建物などを追加したり切り替えたりして遊ぶことができます。

# OTRPについて
OTRPは、Simutransに様々な機能を加えたゲームです。こちらも**オープンソース**となっており、公式がバイナリを提供していないような環境でも自分でビルドすれば遊べます。
ソースコード： https://github.com/teamhimeh/simutrans

# せっかくサーバーを建てるならDockerで簡単にできるようにしたいよね
VPSには既に別のサービスも載っているので、環境を大きく変えるようなことはしたくありませんでした。そこで、Docker上にUbuntuを生成し、そこで動いてもらうことにしました。

# 準備
## ポート開放
Simutransの動作には1ポート必要です。使用したいポート番号を1つ決め、そのポートを開放しておきます。
以下、本書で`[[PORT]]`と記載している箇所はこのポート番号に置き換えて下さい。公式のデフォルトポートは`13353`です。

## GitHubのアカウントの作成・公開鍵の登録
本手順では、ビルド中にソースコードをGitHubからcloneします。その際に認証鍵が必要なので、`ssh-keygen`などで鍵ペアを作成しておきます。※詳しい手順は省略します。
鍵ペアのうち、公開鍵をGitHubの[SSH & GPG Keys](https://github.com/settings/keys)に登録し、秘密鍵は後に使うので分かりやすい場所に置いておきます。

## プレイするバージョンとpakの選定
プレイするOTRPのバージョンを決めておきます。サーバーにアクセスするユーザーは全員同じバージョンを使う必要があります。
[タグ一覧](https://github.com/teamhimeh/simutrans/tags)にあるバージョンを選択して下さい。マイナーバージョンまで全部合わせる必要がある点に注意して下さい。
また、プレイの際に使用するpakファイル群を決めましょう。こちらも、プレイヤー全員が過不足なくpakファイルを導入する必要があります。
以下、本書で`[[SIMUTRANS-VERSION-TAG]]`と記載している箇所はプレイするバージョンに対応するGitHubのタグ(`v35_2`など)に置き換えて下さい。

## 初期マップの作成
サーバーでプレイしたいマップを自身のローカルで作成しておきます。普段のSimutransの新規マップ作成と同様の手順でOKです。もちろん、画像から生成してもよいですし、高度な設定を書き換えてもよいです。マルチプレイ用に特別な設定は不要です。
このときに作成したsveファイルはこの後の手順で使うので、コピーして分かりやすい場所に置いておきましょう。名前は半角英数字のみにしておきましょう。
以下、本書で`[[INITIAL-LOAD-SAVEDATA]]`と記載している箇所はこのファイル名（拡張子`.sve`なし）に置き換えて下さい。

## データ用フォルダの作成
サーバーソース用のフォルダ以外に、データ用のフォルダを1つ作成しておきます。
以下、本書で`[[SIMUTRANS-DATA-PATH]]`と記載している箇所はこのフォルダへの絶対パスに置き換えて下さい。
また、このフォルダの下に、下記のようにファイルを配置しておきます。
```
[[SIMUTRANS-DATA-PATH]]
- config
    - simuconf.tab ※1
- [[PAK-DIR-NAME]]
    - 使用するpakファイルの中身全て
- save
    - [[INITIAL-LOAD-SAVEDATA]].sve ※上記手順で作成した初期ロードファイル
- save-backup ※空フォルダ
- text
    - 使用するpakファイルの中に含まれるtextフォルダの中身をこちらにもコピーしておく
- settings.xml ※自身のローカルにあるXMLファイルをこちらにコピー
```
※1： 自身のローカルにあるconfig/simuconf.tabをデスクトップ等にコピーしてテキストエディタで開き、`singleuser_install`を`0`に置き換えて下さい。サーバーには、この書き換え済みファイルをアップロードしておきます。

## nettoolのパスワード作成
Simutransサーバーに外部からコマンドを叩く仕組みとしてnettoolがありますが、その動作にはパスワードの設定が必要です。
本サーバーでは自動セーブ機能を利用するためにnettoolを使用します。半角英数字のみでパスワードを作成して下さい。
以下、本書で`[[PASSWORD]]`と記載している箇所はこのパスワードに置き換えて下さい。

## 自動セーブ時のPrefix指定
自動セーブで生成されるファイルにPrefixを指定できます。以下、本書で`[[PREFIX]]`と記載している箇所はこのPrefixに置き換えて下さい。
自動セーブのファイルは`[[PREFIX]]-YYYYMMDDhhmmss.sve`というファイル名で保存されます。

## サーバー名指定
Simutransのサーバー名を決めておきます。ドメインである必要はなく、分かりやすい名前を付けて下さい。（詳しいことはよくわかっていません…有識者求む）
以下、本書で`[[SIMUTRANS_SERVERNAME]]`と記載している箇所はこのサーバー名に置き換えて下さい。

# ビルド・実行用ファイルの作成
## docker-compose.yml
```yaml:docker-compose.yml
version: "2"
services:
  main:
    build:
      context: .
      dockerfile: Dockerfile
      target: game
    volumes:
      - "[[SIMUTRANS-DATA-PATH]]:/root/simutrans"
      - "[[SIMUTRANS-DATA-PATH]]/config:/usr/share/games/simutrans/config"
    environment:
      - BACKEND=posix
      - OSTYPE=linux
      - SIMUTRANS_SERVERNAME=[[SIMUTRANS_SERVERNAME]]
      - SIMUTRANS_DEBUG_LEVEL=1
      - PAK_NAME=pak
      - SAVEGAME=[[INITIAL-LOAD-SAVEDATA]].sve
      - SERVER_ADMIN_PASSWORD=[[PASSWORD]]
    ports:
      - [[PORT]]:13353
  auto-backup:
    build:
      context: .
      dockerfile: Dockerfile
      target: auto-backup
    volumes:
      - "[[SIMUTRANS-DATA-PATH]]:/root/simutrans"
    environment:
      # ここの値を書き換える場合はenvvarsファイルも書き換えること
      - SERVER_ADDRESS=main
      - AUTOSAVE_PREFIX=[[PREFIX]]
      - SERVER_ADMIN_PASSWORD=[[PASSWORD]]
```

## docker-compose-load.yml
```yaml:docker-compose-load.yml
version: "2"
services:
  main:
    build:
      context: .
      dockerfile: DockerfileLoad
      target: game
    volumes:
      - "[[SIMUTRANS-DATA-PATH]]:/root/simutrans"
      - "[[SIMUTRANS-DATA-PATH]]/config:/usr/share/games/simutrans/config"
    environment:
      - BACKEND=posix
      - OSTYPE=linux
      - SIMUTRANS_SERVERNAME=[[SIMUTRANS_SERVERNAME]]
      - SIMUTRANS_DEBUG_LEVEL=1
      - PAK_NAME=pak
      - SAVEGAME=[[INITIAL-LOAD-SAVEDATA]].sve
      - SERVER_ADMIN_PASSWORD=[[PASSWORD]]
    ports:
      - [[PORT]]:13353
  auto-backup:
    build:
      context: .
      dockerfile: DockerfileLoad
      target: auto-backup
    volumes:
      - "[[SIMUTRANS-DATA-PATH]]:/root/simutrans"
    environment:
      # ここの値を書き換える場合はenvvarsファイルも書き換えること
      - SERVER_ADDRESS=main
      - AUTOSAVE_PREFIX=[[PREFIX]]
      - SERVER_ADMIN_PASSWORD=[[PASSWORD]]
```

## Dockerfile
```dockerfile:Dockerfile
FROM ubuntu:latest AS base

RUN apt-get update; apt-get install -y --fix-missing git wget make gcc gdb zlib1g-dev libbz2-dev libpng-dev libsdl2-dev libminiupnpc-dev libfreetype6 autoconf simutrans-data curl cron systemctl
COPY id_rsa /root/id_rsa
RUN env GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -F /dev/null -i ~/id_rsa" git clone -n git@github.com:teamhimeh/simutrans.git; cd simutrans; git checkout [[SIMUTRANS-VERSION-TAG]]
COPY config.default /simutrans/config.default
COPY simutrans/tools/ /simutrans/tools
COPY distribute.sh /simutrans/tools/distribute.sh
RUN sh /simutrans/tools/setup-debian.sh
RUN autoconf /simutrans/configure.ac

FROM base AS game
RUN cd simutrans; make -j4 simutrans MSG_LEVEL=4
RUN apt-get install curl
RUN cd simutrans; chmod 755 tools/distribute.sh; tools/distribute.sh
RUN mv /simutrans/build/default/sim /usr/share/games/simutrans/simutrans
CMD ["bash", "-c", "cd /root/simutrans; /usr/share/games/simutrans/simutrans -use_workdir -objects $PAK_NAME -server -log 1 -debug $SIMUTRANS_DEBUG_LEVEL -server_admin_pw $SERVER_ADMIN_PASSWORD -lang ja"]

FROM base AS auto-backup
RUN cd simutrans; make -j4 nettool
RUN mv /simutrans/build/default/nettool/nettool /usr/share/games/simutrans/nettool
COPY ./crontab-task /crontab-task
COPY run-backup.sh /run-backup.sh
RUN chmod 775 /run-backup.sh
RUN cat /crontab-task | crontab -
COPY ./envvars /etc/environment
CMD ["cron", "-f"]
```

## DockerfileLoad
```dockerfile:DockerfileLoad
FROM ubuntu:latest AS base

RUN apt-get update; apt-get install -y --fix-missing git wget make gcc gdb zlib1g-dev libbz2-dev libpng-dev libsdl2-dev libminiupnpc-dev libfreetype6 autoconf simutrans-data curl cron systemctl
COPY id_rsa /root/id_rsa
RUN env GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -F /dev/null -i ~/id_rsa" git clone git@github.com:teamhimeh/simutrans.git; cd simutrans; git checkout [[SIMUTRANS-VERSION-TAG]]
COPY config.default /simutrans/config.default
COPY simutrans/tools/ /simutrans/tools
COPY distribute.sh /simutrans/tools/distribute.sh
RUN sh /simutrans/tools/setup-debian.sh
RUN autoconf /simutrans/configure.ac

FROM base AS game
RUN cd simutrans; make -j4 simutrans MSG_LEVEL=4
RUN apt-get install curl
RUN cd simutrans; chmod 755 tools/distribute.sh; tools/distribute.sh
RUN mv /simutrans/build/default/sim /usr/share/games/simutrans/simutrans
CMD ["bash", "-c", "cd /root/simutrans; /usr/share/games/simutrans/simutrans -use_workdir -objects $PAK_NAME -server -log 1 -debug $SIMUTRANS_DEBUG_LEVEL -server_admin_pw $SERVER_ADMIN_PASSWORD -load $SAVEGAME -lang ja"]

FROM base AS auto-backup
RUN cd simutrans; make -j4 nettool
RUN mv /simutrans/build/default/nettool/nettool /usr/share/games/simutrans/nettool
COPY ./crontab-task /crontab-task
COPY run-backup.sh /run-backup.sh
RUN chmod 775 /run-backup.sh
RUN cat /crontab-task | crontab -
COPY ./envvars /etc/environment
CMD ["cron", "-f"]
```

## envvars
```text:envvars
SERVER_ADDRESS=main
AUTOSAVE_PREFIX=[[PREFIX]]
SERVER_ADMIN_PASSWORD=[[PASSWORD]]
```

## run-backup.sh
```sh:run-backup.sh
#!/bin/sh

# 予告メッセージ
. /etc/environment
/usr/share/games/simutrans/nettool -s $SERVER_ADDRESS -p $SERVER_ADMIN_PASSWORD say "1分後に自動セーブをします"
sleep 50
/usr/share/games/simutrans/nettool -s $SERVER_ADDRESS -p $SERVER_ADMIN_PASSWORD say "10秒後に自動セーブをします"
sleep 10

# セーブ実行
/usr/share/games/simutrans/nettool -s $SERVER_ADDRESS -p $SERVER_ADMIN_PASSWORD force-sync
wait
sleep 10

# 保存されたファイルを退避
cp -f /root/simutrans/server13353-network.sve /root/simutrans/save-backup/${AUTOSAVE_PREFIX}-`date "+%Y%m%d%H%M%S"`.sve
```
※公開するポート番号を変える場合でも、本ファイル内の`13353`は書き換えないで下さい。

## crontab-task
```text:crontab-task
PATH=/usr/bin:/usr/sbin:/home/imasami/.rbenv/shims/:$PATH
SHELL=/bin/bash
59 1-23/2 * * * source /usr/share/export-env; /run-backup.sh > /var/log/auto-backup.log 2>&1
```
※偶数時0分にセーブを実行する例です。サーバー内への予告のために1分前からファイルを実行するため、このような記法となっています。自動セーブの間隔・タイミングは調整できます。

# ビルド・実行用ファイルの配置
データ用フォルダとは別に、実行用フォルダを作成します。（今後、サーバーの起動などのタイミングでこのフォルダにアクセスすることがあるため、アクセスしやすく忘れにくい場所にしておくことをおすすめします。）
このフォルダの下に、下記のファイルを配置します。
```
（実行用フォルダ）
- simutrans
    - tools
        - ※本家Simutransのソースコードにある`simutrans/tools`フォルダの中身をここにコピーする
- config.default ※本家Simutransのソースコードにある`simutrans/config.default`ファイルをここにコピーする
- crontab-task
- distribute.sh ※本家Simutransのソースコードにある`simutrans/tools/distribute.sh`ファイルをここにコピーする
- docker-compose.yml
- docker-compose-load.yml
- Dockerfile
- DockerfileLoad
- envvars
- id_rsa ※GitHubにアクセス可能な秘密鍵をここにコピーする
- run-backup.sh
```

# ビルドと初回ロード
初回起動では、サーバーにデータをうまく配置するために一度`docker-compose-load.yml`でビルド・ロードします。
## 1. ビルド・起動
※root権限で実行すること
```sh
cd (実行用フォルダ)
docker-compose -f docker-compose-load.yml up --build
```
上記コマンドを実行すると、まずはソースコードのビルドが始まります。スペックにもよりますが10分～20分ほどかかります。
その後、`main_1 | Simutrans version xxx.x.x Nightly from Xxx xx xxxx rxxxxxx`のようなログが流れたら、まずはサーバーが動き出したことがわかります。その後、アプリケーションがpakファイルを読み込み始めます。
コマンド実行後、`***_main_1 exited with code 0`という行が表示されたら**何らかの問題が起きてアプリがクラッシュしている**のでトラブルシューティングを行います。

## 2. サーバーに一度アクセス
Simutransのアプリを自身の端末で起動し、一度サーバーに接続します。接続してマップが動き出したら切断しておいて下さい。
pakファイルが多いor大きいと、サーバーの読み込み時間が長くなり、起動後しばらく接続できない場合があります。
※接続テストだけでなく、必ず接続して下さい。接続のタイミングでサーバー用ファイルの生成が完了します。

## 3. 初回ロードが成功しているか確認
`[[SIMUTRANS-DATA-PATH]]`直下に`server13353-network.sve`と`server13353-pwdhash.sve`の2ファイルが生成されていることを確認します。
その後、`docker-compose`コマンドは`Ctrl + C`で終了しておきます。安全のため、次のステップに進む前に、同じディレクトリで`docker-compose -f docker-compose-load.yml down`を実行しておくとよいでしょう。

# サーバー本起動
※root権限で実行すること
```sh
cd (実行用フォルダ)
docker-compose up --build -d
```
※こちらには`-f docker-compose-load.yml`がありません！
※初回は末尾の`-d`を付けずに起動し、pak・マップファイルが正しく読み込めるか確認したほうがよいでしょう。確認が取れたら`Ctrl + C`で一度アプリを終了し、再度`-d`付きで実行しましょう。

これで、初回ロード時に生成された`server13353-network.sve`を読み込んでくれます。

# サーバーアプリダウン時・マシン再起動後
※root権限で実行すること
```sh
cd (実行用フォルダ)
docker-compose up --build -d
```
で最新のバックアップを読み込んでくれます。

# トラブルシューティング
## 起動時にクラッシュする
クラッシュ直前のエラーメッセージが`ERROR: modal_dialogue: called without a display driver => nothing will be shown!`となっている場合、実行時（セーブデータ読み込み時の可能性大）に何らかのエラーが発生しています。
- セーブデータのファイルの配置に問題はないか、ファイルがローカルアプリで正常に読み込めるかを確認して下さい。
    - ここで読み込めるか確認すべきファイルは、初回ロード時の場合は`[[SIMUTRANS-DATA-PATH]]/save`直下の`[[INITIAL-LOAD-SAVEDATA]].sve`、本起動時の場合は`[[SIMUTRANS-DATA-PATH]]`直下に生成されている`server13353-network.sve`です。
- `simuconf.tab`の`singleuser_install`が0になっていることを今一度確認して下さい。
- `server13353-network.sve`を直接書き換えていませんか？
    - もし書き換えてしまった場合、初回ロードからやり直してください。

なお、このエラー自体は非常に汎用的なものです。

補足：`server13353-network.sve`はクライアントで読み取ることが可能ですが、逆にこのファイルを直接書き換えることはできません。（読み込みエラーが発生します）

余談：このエラーは語弊を恐れずに言うと「エラーダイアログを出したいんだけど、出すモニターがないよ！」というものです。通常、ファイルの読み込みエラーなどはゲーム画面に表示されますが、Headlessサーバーのためモニターがありません。このようなエラーが起きたとき、Simutransは表示しようとしていたエラー文を提示してくれません。
この仕様が問題発生時の調査を難航させた原因でして、`make`に渡す`MSG_LEVEL`やsimutrans実行ファイルの`-debug`に渡す`SIMUTRANS_DEBUG_LEVEL`を変え、ときにはソースコードにログ関数を仕込んで調査し、などと手こずりました…

## 初回ロード後、`server13353-network.sve`と`server13353-pwdhash.sve`が生成されない
初回起動時に手元のアプリでサーバーにアクセスし、**マップを表示する**ところまで行いましたか？
Simutransのサーバーには、誰かがサーバーにアクセスした時点で自動セーブを行う機能があり、サーバー用ファイルはこれを使って生成しています。

## パラメータを書き換えたが反映されない
- docker-composeのup時に`--build`を忘れていませんか？
- `docker-compose down`をし忘れていませんか？