この記事はSimutrans Advent Calendar 2025の17日目のものです。

はじめに

Netsimutrans(以下NS)はさまざまな方法で構築でき、この大遅刻記事が参加しているSimutrans Advent Calendar 2025に限ってもWindows環境×ポート開放、ポート開放不要、Linux環境(Extended)など、OS・Simutransの種類・インターネットへの公開方法の組み合わせがそれぞれ異なる構築方法が紹介されています。

本稿ではLinuxでDockerを使い、ヘッドレスOTRP・Discord Bot・nginxを動かすことで、Simutrans World Monitorが使えてアドオン追加はDiscordサーバーにPakファイルを投げて更新はbatファイルを叩いて待っていれば済むNSの構築の仕方を話していきます。

ヘッドレスなOTRPとアドオン追加・更新の自動化はそれぞれ詳しい記事がすでに存在するのですが、環境など細かいところが異なっているので改めて取り上げたいと思います。

…こういう考えの人がいるせいで、個別具体的な記事が増えていくのでしょう。そこで本稿では個別具体的でない記事ということをアピールするためはじめにNSのインターネットへの公開方法を一通りまとめてみました。

質問に答えるだけでNSが立つフローチャート

  • 質問1 サーバーとして使える物理PCとネット環境はありますか?
    • はい:質問2へ
    • いいえ:VPSサーバーを契約しましょう
  • 質問2 ポート開放はできますか?
    • はい:お好きな番号を開放しましょう
    • いいえ・分からない:質問3へ
  • 質問3 ネットの接続方法はIPoE(IPv4 over IPv6やMAP-E、OCNバーチャルコネクトやv6プラスみたいな名前)ですか?
    • はい:利用可能ポート番号の範囲から解放しましょう
    • いいえ:以下のどちらかを選んでください
      • GCEの無料枠でVPNサーバーを立てる
        • [デメリット]レイテンシ(遅延)が増える
      • 参加者にCloudflaredかTailscaleを入れてもらう
        • [デメリット]入れてもらう手間・申し訳なさの発生

質問1 サーバーとして使える物理PCとネット環境はありますか?

NSをしたい期間(24時間365日かもしれないし、毎週土曜朝に起動して日曜夜に落とすだけで事足りるかもしれない)に起動し続けられるPCとネット環境さえあれば、NSは無料で立てることができます。

ポート開放によるリスクを度外視すれば、ポートが開けられないだけでは財布を開こうとしなくていいのです。

なお、2.4GHzのWi-Fiでネットに繋げているために親が電子レンジを使うと通信が切れるのならルーターから有線で繋げられる位置にPCを移動し、雷雲が近づくと親がルーターのコンセントごと引っこ抜いてしまうのなら説得してやめてもらいましょう。

居住形態によってこんな風に立ちはだかる大小の壁を乗り越えられなさそうであれば、以下の記事のようにVPSサーバーを借りてNSを立てることになります。

質問2 ポート開放はできますか?

ポート開放が可能ならもはや言うことはありませんが、セキュリティリスクを考えると13353番が有名なポート番号かどうかは置いておいて別の番号を開放し、参加者にはポート番号付きのサーバーアドレスを打ってもらう方がいいかもしれません。

またいずれも後述しますが、レイテンシ(遅延)を受け入れるのならVPNを立てる、あるいは参加者に別途ソフトを入れてもらい家の物理PCはポート開放しないでおくのが一番安全といえます。

質問3 ネットの接続方法はIPoEですか?

まずネットの接続方法はこのサイト(OCN IPoE接続環境確認サイト)か、ルーターの設定画面、最悪プロバイダーとの契約書で確認できます。IPoEやIPv4 over IPv6などとあれば、ルーターの設定画面やこのサイト(IPv6(MAP-E方式)使用可能ポート確認)で確認できる利用可能ポート番号の中でポート開放ができます。

利用可能ポート番号に13353が含まれていることは稀なので、上述したようにサーバーアドレスはポート番号付きになるほか、IPoEでもDS-Liteというものだった場合ポート開放ができません。

これは、IPoEという接続方法は普通の方法(PPPoEという)と違いグローバルIPv4アドレスをほかのユーザーと共有しており、MAP-Eという方式ではそのアドレスのポート番号のうちいくつかが利用可能ポートとして割り当てられるのに対し、DS-Liteでは割り当てられないためです。

NECのルーターではホーム>情報>現在の状態の下の方に利用可能ポート一覧がある

あとは普通にポート開放ができる場合と手順は同じです。

ポート開放できないときの選択肢 すなわち最も安全な択

DS-Lite方式でネットに繋いでいるなどでポート開放ができない場合、またセキュリティリスクを考えるとNSの立てるには以下の選択肢が残ります。いずれも無料なのでNSを立てないという道はありません。

GCEの無料枠でVPNサーバーを立てる

Googleのクラウドサービス Google CloudのCompute Engineには無料枠があり、2025年現在米国リージョンのe2-micro(一番低いスペック)インスタンスおよび外部IPv4アドレスを永続的に無料で利用できます。

無料枠のインスタンスをVPSサーバーとして使うのは無理がありますが、IPv4アドレスが付いてくるのでここで通信を受け取り、自宅PCに転送するVPNサーバー、いわゆる踏み台サーバーとして使うことができます。

ポート開放が不要で、VPNサーバーと自宅PCとの通信はTailscaleやZeroTierなどですればポート開放するよりもセキュリティリスクは数倍低くなります。

問題はNSと参加者双方が日本に居てもVPNサーバーを経由、すなわち太平洋を往復しながら通信するためレイテンシ(遅延)が増えてしまうことで、リージョンを日本に変更すると月額課金が発生してしまうことです。

私はこの方法で身内マイクラ鯖を動かしていましたが、日本リージョンにしてから月あたり1000円前後が持っていかれていました。

この方法でNSを立てている記事がないためマイクラ鯖の例となりますが、動かすゲームが違うだけで同じTCP通信なので参考にはできると思います。遅延のなかでのSimutransの操作感は分かりませんが。

参加者にCloudflaredかTailscaleを入れてもらう

最終手段かつ最も安全な方法といえるのがこれで、Cloudflare TunnelではNSと参加者間に通信のトンネルを作り、Tailscaleでは仮想ネットワーク内でNSと参加者が通信する形で、サーバー側のポート開放なしでNSが実現できます。いずれも参加者にはCloudflaredかTailscaleアプリを入れてもらわないといけません。

Cloudflareでは独自ドメインが必要なうえ、接続するためにコマンドプロントなどでコマンドを打ってもらう必要がありますが、Tailscaleではサーバー機の共有リンクを踏んでもらえば、以降はサーバー機のTailscale上のIPアドレスを入れるだけで接続が可能です(Tailscaleが裏で起動してる必要はあり)。

なお、Tailscaleの共有はネットワーク丸ごと共有するのと各マシンを共有するの(Node Sharing)の2つがあり、前者は無料プランだと共有できる人数が3ユーザーまでなのですが、NSをやるにあたってはNode Sharingで十分なはずです。

いずれかの方法を取れば無料で(一部除く)NSが立てられます! 今日からあなたも鯖主です!!!!

公式wiki以外のすべてを疑え

いよいよ本題のヘッドレスOTRPのビルドに移りますが、冒頭で触れたようにDockerでのビルドは先駆者様がいます。

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

ではこの記事を見ればよいですね。解散!

…とはいきません。見出しに書いてあることを読んでもらいたいのですが、2025年現在のLinux環境でのビルドではconfig.defaultを書き換える必要はなくなっているため、ネット上に数多くあるビルド方法のほぼすべては過去のものといえるのです(日本語wikiの記載変えたほうがよくない?)。

そのため、先駆者様の記事も疑ってみましょう。公式Wikiのビルド方法によると、makeでのビルドは必要なライブラリを入れた後に

autoconf
./configure
make -j $(nproc)

と打てばビルドに進めるようです。なお、実際にはautoconfを実行すると

configure.ac:175: warning: AC_OUTPUT should be used without arguments.
configure.ac:175: You should run autoupdate.

と言われるのでautoupdateをしてからのautoconfとなります。

ところが同記事のDockerfileではmakeの前にしているのはautoconf /simutrans/configure.ac(autoconfで付けられた実行権限でconfigureを叩く場面のところ、叩いてるのはconfigure.acだし、autoconfは引数なしで動くはず)のみで、別途sedコマンドなどでconfig.defaultの書き換えもしてないようです。そのため、makeしても

Makefile:34: *** Unkown BACKEND "", must be one of "gdi sdl sdl2 mixer_sdl mixer_sdl2 posix".  Stop.

というおそらくSimutransのビルドに挑戦した人が最も多く見るであろうエラーが返されます。

人様の記事を疑ったので、この記事も疑ってみましょう。buildできないDockerfileのままで良しとする人はそんなに存在しないと考えられるため、この記事ではうそを言って先駆者様の記事を貶してるといえます。

そこで、以下にこの記事においてちゃんと動くと主張されているDockerfileを示しますので、先駆者様のDockerfileと併せて

docker run -dit ubuntu:latest
docker exec -it (コンテナ名orID)

して、それぞれの内容を実行してみてmakeが通るか試してみてください。

ちなみに、Windows環境ではSimutransのexeファイルをサーバーモードで実行すればソースコードからビルドという七面倒な手順を踏まずにNSを立てれるので、Linux環境でもできないかとOTRPのReleaseから最新の実行ファイルを取ってきました。以下のDockerfileなどで動作中のNSのファイル群に置いてビルドしたsimの代わりに実行してみると、

./sim-linux-OTRPv48_2: error while loading shared libraries: libminiupnpc.so.17: cannot open shared object file: No such file or directory

よく分からないのですがダメみたいです。

Dockerコンテナなんもわからん でも使ったほうがいい

NSの実行ではsystemdnohupなどでずっと実行されているようにすることが大切ですが、たった5MBの実行ファイルのためだけにDocker使うんですか???(意訳)の声を跳ねのけてDockerを使いましょう。アドオンフォルダやセーブデータを適切に管理しておけば、本体バージョンのアップデートなどが簡単に行えます。

Dockerfile

FROM ubuntu:latest

ARG SIMUTRANS_VERSION

# SIMUTRANS_VERSIONが設定されていない場合はビルドを失敗させる
RUN if [ -z "${SIMUTRANS_VERSION}" ]; then \
        echo "Error: SIMUTRANS_VERSION build argument is not set. Please use --build-arg SIMUTRANS_VERSION=<version>."; \
        exit 1; \
    fi

ENV SIMUTRANS_VERSION=${SIMUTRANS_VERSION}

# 必要なパッケージのインストール
RUN apt-get update && apt-get install -y \
    wget unzip autoconf zstd make gcc g++ \
    zlib1g-dev libbz2-dev libpng-dev cron curl vim \
    libsdl2-dev xvfb x11vnc websockify python3-websockify novnc fluxbox

# simutransのソースコードのダウンロード、展開
WORKDIR /tmp
RUN wget "https://github.com/teamhimeh/simutrans/archive/refs/tags/${SIMUTRANS_VERSION}.zip" && \
    unzip "${SIMUTRANS_VERSION}.zip"

# Simutransのビルドディレクトリに移動
# v44_3.zip が simutrans-44_3/ に展開されることを想定し、
# ${SIMUTRANS_VERSION#v} を使って動的にパスを生成
WORKDIR "/tmp/simutrans-${SIMUTRANS_VERSION#v}"

RUN autoupdate && \
    autoconf && \
    ./configure && \
    make DEBUG=1

# nettoolsのビルド
WORKDIR "/tmp/simutrans-${SIMUTRANS_VERSION#v}/nettools"
RUN make

# get_lang_files.sh の実行とtextフォルダの取得
WORKDIR "/tmp/simutrans-${SIMUTRANS_VERSION#v}"
RUN ./get_lang_files.sh

# 最終的なディレクトリを用意
WORKDIR /app

# 1. sim実行ファイルのコピー
RUN cp "/tmp/simutrans-${SIMUTRANS_VERSION#v}/sim" .

# 2. nettoolsフォルダ全体のコピー
RUN cp -r "/tmp/simutrans-${SIMUTRANS_VERSION#v}/nettools" .

# 3. textフォルダのコピー(実行ファイルと同じ階層へ)
RUN cp -r "/tmp/simutrans-${SIMUTRANS_VERSION#v}/simutrans/text" /app/

RUN cp -r "/tmp/simutrans-${SIMUTRANS_VERSION#v}/simutrans/font" /app/
RUN cp -r "/tmp/simutrans-${SIMUTRANS_VERSION#v}/simutrans/themes" /app/
RUN cp -r "/tmp/simutrans-${SIMUTRANS_VERSION#v}/simutrans/script" /app/

COPY save.sve /app/server13353-network.sve

COPY sqai_hm_monitor /app/ai/sqai_hm_monitor/

COPY start.sh /app/start.sh
COPY savecron /app/savecron
COPY save.sh /app/save.sh

RUN chmod +x /app/start.sh /app/save.sh

# ビルド成果物を除いた中間ファイルを削除
RUN rm -rf /tmp/*

# コンテナ起動時に実行するコマンド
CMD ["/app/start.sh"]

start.sh

#!/bin/bash

set -e

cd /app

crontab /app/savecron

cron

/app/sim -server 13353 -server_admin_pw 0000 -objects pak.nippon -log 1 -debug 4 -lang ja -singleuser -load ../server13353-network.sve

save.sh

#!/bin/bash

SAVE_DIR="/app/autosave"
SAVE_FILE="/app/server13353-network.sve"
NETTOOL="/app/nettools/nettool"

mkdir -p "$SAVE_DIR"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] save.sh started"

"$NETTOOL" -p 0000 say "10秒後に自動セーブを行います。"
sleep 10

"$NETTOOL" -p 0000 force-sync
sleep 10

cp "$SAVE_FILE" "$SAVE_DIR/$(date '+%Y%m%d-%H%M').sve"

#sveファイルが100個より多くなったら1個消す
file_count=$(find "$SAVE_DIR" -maxdepth 1 -type f -name '*.sve' | wc -l)

if [ "$file_count" -gt 100 ]; then
    old_file=$(find "$SAVE_DIR" -maxdepth 1 -type f -name '*.sve' -printf '%T@ %p\n' | sort -n | head -n 1 | awk '{print $2}')
    if [ -n "$old_file" ]; then
        rm -v "$old_file"
    fi
fi

savecron

*/30 * * * * /app/save.sh >> /app/save.log 2>&1

私が運営をさせてもらっている、とあるNSで実際に使っているヘッドレスOTRPコンテナのためのDockerfileとなかまたちはこんな感じです。

git cloneすればsimutransフォルダとしてDLできるのでフォルダ名周りのややこしい処理が要らなくなるほか、save.shでは10日でsveファイルが削除される欠陥構造のため、コピペしてそのまま使えるとはいえません。生成AIに渡してこの段落の文章を申し送り事項として送り付ければ直してくれることでしょう

なお、ヘッドレスなのにDockerfileにてGUIライブラリなどをインストールする記述がありますが、これはSimutrans World Monitorで必要なAIプレイヤーが追加されたセーブデータをWindows環境で用意するのがパスの都合で面倒だったので、コンテナ内でGUIのSimutransを立ち上げAIプレイヤーを追加できるよう、NoVNCを実行できるようにしているためです。

参考)このコンテナ内でNoVNCを使いGUIのSimutransを起動する

Xvfb :99 -screen 0 1024x768x24 &
export DISPLAY=:99
x11vnc -display :99 -forever -nopw -shared -bg
git clone https://github.com/novnc/noVNC.git /opt/novnc
/opt/novnc/utils/novnc_proxy --vnc localhost:5900 --listen 6080 &
DISPLAY=:99 ./sim -server 13353 -server_admin_pw 0000 -objects pak.nippon -log 1 -debug 2 -lang ja -singleuser -load ../hogehoge.sve

これらのライブラリを入れると当然ながらヘッドレスではなくなるので、configureしたときの表記がWARNING: No backend found, using server (posix)!からWARNING: Using SDL2 backend!になります。要検証ですがバックエンドがSDL2になってもヘッドレスで動いているため、多分サーバーの動作には影響がない…はず?(環境がUbuntu Serverなのでたまたまいけてるのかも)


sudo docker run -d -p 13353:13353 -p 6080:6080 --restart=always --name hoge -v [アドオンのフォルダのパス]:/app/pak.nippon -v [任意のパス(Discord Botコンテナとcmd.txtなどをやりとりするため)]:/app/file_io --log-opt max-size=100m --log-opt max-file=5 simutrans-server:v46

このDockerfileのコンテナを立ち上げるときのコマンドは以上のとおり。NoVNCを実行するため6080番も開けていますね。このコマンドを実行するときは、同じディレクトリにstart.shなどのファイル群やSimutrans World Monitorの中身であるsqai_hm_monitorフォルダ、セーブデータとしてsave.sveを据えておく必要があります。

ここで、セーブデータもアドオンのようにマウントすればやり取りが楽になると思った方もいると思いますが、私の環境だとセーブデータをマウントすると以下の動画のようにNSに誰かが入ると先に入ってた人が切断されてしまいました。

ただし、save.shでセーブデータが保存されるフォルダ(/app/autosave)はホストのどこかにマウントしておくべきなので、このコマンドも要修正ものです。

なお、sqai_hm_monitor/libs/global.nutの各ファイルのパスは本稿の環境では"/app/file_io/cmd.txt"などのように変更しないとAIプレイヤー追加時にエラーとなります。

Docker Composeを使えばほかのコンテナと併せてデータのやり取りが楽になりそうですが、docker-compose,ymlはitzg/docker-minecraft-serverでちょっと触ったくらいの知識しかないのでまだ勉強中です。また、あるときからNS内のログを取るようにしたところサーバー機のストレージ全体がログファイルで溢れかけたので、ログファイルは最大500MB(100MBで区切って5個まで保持)となるようにしています。

以上でヘッドレスOTRPのコンテナを作ることができました。ここからは、コマンドにてマウントしてるfile_ioフォルダとファイルのやり取りをするSimutrans World MonitorのDiscord Botのコンテナを用意します。

Discord Botのコンテナ

Dockerfile(生成AI製だったはず なぜgitを入れてるか分からない)

FROM python:3.11-slim

# 必要なパッケージをインストール
RUN apt-get update && apt-get install -y \
    git \
    && rm -rf /var/lib/apt/lists/*

# 作業ディレクトリを設定
WORKDIR /app

# requirements.txt をコピーして依存関係をインストール
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# Botのソースコードをコピー
COPY . .

# Botの起動コマンド
CMD ["python", "monitor.py"]

monitor.py(加筆した部分のみ)

ALLOWED_CHANNELS = [1234567891234657891,1234567891234657891]

ALLOWED_TYPES = ['.tab','.pak','.zip']
SAVE_DIR = '/app/data'

intents = discord.Intents.default()  # デフォルトのインテントを使う
intents.message_content = True

client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
GUILD_ID = 1234567891234567891

@tree.command(name="nurupo", description="駅員が反応します。", guild=discord.Object(id=GUILD_ID))
async def ping(interaction: discord.Interaction):
    if interaction.channel_id not in ALLOWED_CHANNELS:
        await interaction.response.send_message(
            "コマンドは指定のチャンネルでの呼び出しをお願いします。",
            ephemeral=True
        )
        return
    await interaction.response.send_message("ガッ")

@tree.command(name="download_sve", description="最新のセーブデータを送信します。データは毎時5分・45分更新です。", guild=discord.Object(id=GUILD_ID))
async def download_sve(interaction: discord.Interaction):
    if interaction.channel_id not in ALLOWED_CHANNELS:
        await interaction.response.send_message(
            "コマンドは指定のチャンネルでの呼び出しをお願いします。",
            ephemeral=True
        )
        return
    await interaction.response.defer()

    autosave_dir = Path("/app/autosave")

    sve_files = list(autosave_dir.glob("*.sve"))
    latest_file = max(sve_files, key=lambda f: f.stat().st_mtime)

    await interaction.followup.send(file=discord.File(latest_file))

# メッセージ受信時に動作する処理
@client.event
async def on_message(message):
    # メッセージ送信者がBotだった場合は無視する
    if message.author.bot:
        return

    if not message.channel.id in ALLOWED_CHANNELS:
        return

    if message.attachments:
        for attachment in message.attachments:
            file = attachment.filename

            if any(file.lower().endswith(ext) for ext in ALLOWED_TYPES):
                # ファイルを保存
                save_path = os.path.join(SAVE_DIR, file)
                await attachment.save(save_path)
                print(f"{file} を保存しました。")
                await message.add_reaction("👍")

コンテナの立ち上げコマンドは次のとおり。

sudo docker run -d --restart=always --name discord-bot -v [任意のパス(Discord Botコンテナとcmd.txtなどをやりとりするため)]:/app/file_io -v [受け取ったpakファイルを置く場所]:/app/data discord-bot:latest

Simutrans Wolrd Monitorを使用していると(要検証)、クライアント側に保存されるclient-network.sveのようなセーブデータがローカルで開けなくなる現象があり、サーバー側のオートセーブを持ってきて投稿するスラッシュコマンドをこさえていますが、セーブデータがDiscordの容量制限のせいで投稿できないため凍結されています。

また、リポジトリのreadmeではSimutransとDiscord Botの起動順序の言及がありますが、私の環境ではどちらが先でも動いてくれました。

アドオン追加・更新を自動化

以下のスクリプト類は生成AIの力を借りて突貫で作成したもののため、「こうやってるのがあるらしい」と生成AIに投げるために使う程度に留めたほうがよいです。

nginxのwebサーバー

docker-compose.yaml

version: '3.8'
services:
  nginx:
    image: nginx:stable-alpine
    container_name: simutrans_web_server
    ports:
      - "8080:80"
    volumes:
      - [任意のフォルダ]:/usr/share/nginx/html/pak_update:ro
    restart: always

このwebサーバーをtailscale funnelで公開します(コマンドは書きとめてなかったようです)。

参加者に実行してもらうスクリプト

setup.bat(OTRPのバージョン確認が欠陥)

@echo off

cd /d "%~dp0"

set SIMUTRANS_VER=122-0
set OTRP_VER=v44_3

set OUTPUT_FILE=simuwin-%SIMUTRANS_VER%.zip
set OTRP_OUTPUT_FILE=sim-WinGDI64-OTRP%OTRP_VER%.zip

echo simutransをセットアップします
if not exist simutrans.exe (
    echo simutransをダウンロードします
    curl -L -o "%OUTPUT_FILE%" "https://sourceforge.net/projects/simutrans/files/simutrans/%SIMUTRANS_VER%/simuwin-%SIMUTRANS_VER%.zip/download"

    powershell -Command "Expand-Archive -Path '%OUTPUT_FILE%' -DestinationPath '.' -Force"
    robocopy simutrans . /E /MOVE
    rmdir simutrans
    del %OUTPUT_FILE%
    echo simutransのセットアップが完了しました
) else (
    echo simutransのセットアップは完了しています
)

echo.
echo OTRPをセットアップします
if not exist sim-WinGDI64-OTRPv44_3.exe (
    echo OTRP %OTRP_VER% をダウンロードします
    curl -L -o "%OTRP_OUTPUT_FILE%" "https://github.com/teamhimeh/simutrans/releases/download/%OTRP_VER%/sim-WinGDI64-OTRP%OTRP_VER%.zip"

    powershell -Command "Expand-Archive -Path '%OTRP_OUTPUT_FILE%' -DestinationPath '.' -Force"
    del %OTRP_OUTPUT_FILE%
    echo OTRPのセットアップが完了しました
) else (
    echo OTRPのセットアップは完了しています
)

echo.
echo セットアップが完了しました
echo 続いて、simutrans-updater.batを実行してpakファイルをダウンロードしてください。
pause

simutrans-updater.bat

@echo off

REM ここに起動したいsimutransのファイル名を書く(例:simutrans.exe、sim-WinGDI64-OTRPv44_3.exeなど)
set SIMUTRANS_PATH=sim-WinGDI64-OTRPv44_3.exe

set PAK_FOLDER=pak

set VERSION_URL=https://hoge.tail123abc.ts.net/pak_update/pakversion.txt
set ZIP_URL=https://hoge.tail123abc.ts.net/pak_update/release.zip

cd /d "%~dp0"

REM exeの存在確認
if not exist "%SIMUTRANS_PATH%" (
    echo 実行ファイル %SIMUTRANS_PATH% が見つかりません。batファイルをメモ帳で開き、SIMUTRANS_PATHに起動したいsimutransのファイル名を書いてください(例:simutrans.exe、sim-WinGDI64-OTRPv44_3.exeなど)
    pause
    exit /b
)

REM ローカルのpakversionの存在確認
echo ローカルのバージョンを確認しています
set LOCAL_VER=0
if exist pakversion.txt (
    set /p LOCAL_VER=<pakversion.txt
)

echo %LOCAL_VER%

REM webサーバー上のバージョン取得
echo 更新情報を読み込みます
curl -s %VERSION_URL% > pakversion.tmp
set /p WEB_VER=<pakversion.tmp
del pakversion.tmp
echo %WEB_VER%

set /a LOCAL_NUM=%LOCAL_VER%
set /a WEB_NUM=%WEB_VER%

if %LOCAL_NUM% GEQ %WEB_NUM% (
    echo ローカルのバージョンは最新です
) else (
    echo 新しいバージョンが存在します。pakの更新をしています...

    if exist "%PAK_FOLDER%" (
        rmdir /s /q "%PAK_FOLDER%"
    )

    powershell -Command "(New-Object System.Net.WebClient).DownloadFile('%ZIP_URL%', 'release.zip')"
    powershell -Command "Expand-Archive -Path 'release.zip' -DestinationPath '.' -Force"
    del release.zip

    > pakversion.txt echo %WEB_VER%

    echo アップデートが完了しました
)

echo ゲームを起動します
start "" "%SIMUTRANS_PATH%" -objects "%PAK_FOLDER%"

アドオンを追加する側の動作

やってることは先駆者様と同じでほぼ真似てるといえるのでアドオンをリポジトリで管理しています。ただGithub Actionがうまくいかず追加手順は手動です。

更新手順(Windows環境)

git add .
git commit -m "変更点をここに書く"
git tag -a 20251217 -m "追加分"
git push origin --tags

after_push.bat

@echo off
chcp 65001 >nul

set "MAKEOBJ_PATH=...\makeobj-win-60-7\makeobj.exe"

cd /d "%~dp0"

REM Gitリポジトリか確認
git rev-parse --is-inside-work-tree >nul 2>&1
if errorlevel 1 (
    echo このフォルダはGitリポジトリではありません。
    pause
    exit /b 1
)

REM 最新コミットに付いているタグを取得(なければ空)
for /f "delims=" %%T in ('git tag --points-at HEAD') do (
    echo %%T > pakversion.txt
    goto :done
)

REM タグが無ければファイル作成せず通知(必要なら空ファイルを作ってもよい)
echo 最新のコミットにはタグが付いていません。
goto :eof

:done
echo 最新のタグを pakversion.txt に出力しました。
type pakversion.txt

if exist "release" (
    echo release フォルダを空にします。

    del /q /f "release\*"
    for /d %%D in ("release\*") do rd /s /q "%%D"

    echo 完了しました!
) else (
    echo release フォルダが見つかりません。
)

REM pakフォルダを作成
if not exist "release\pak" mkdir "release\pak"

REM ground.Outsideを移動してmerge、その他もろもろをコピー
copy /Y "pak-data\ground.Outside.pak" "release\pak\"
"%MAKEOBJ_PATH%" merge "release\pak\boot.pak" "pak-data\*.pak"
robocopy "pak-data" "release\pak" /E /XF *.pak

REM PowerShellのCompress-Archiveを使ってzipに
powershell -Command "Compress-Archive -Path 'release\*' -DestinationPath 'release.zip' -Force"

REM scpでzipとpakversionをwebサーバーに配置
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "release.zip" [サーバー機のWebサーバーのパス]

if %ERRORLEVEL% EQU 0 (
    echo SCP転送成功
) else (
    echo SCP転送失敗 エラーコード: %ERRORLEVEL%
)

scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "pakversion.txt" [サーバー機のWebサーバーのパス]

if %ERRORLEVEL% EQU 0 (
    echo SCP転送成功
) else (
    echo SCP転送失敗 エラーコード: %ERRORLEVEL%
)

REM sshでpakフォルダの中身を消してscpで新しいpakフォルダの中身を転送してくる
ssh "rm -rf [サーバー機のアドオンフォルダ]"
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -r "release\pak\." [サーバー機のWebサーバーのパス]

REM dockerコンテナの再起動
ssh "sudo docker restart hoge"
pause

おわりに

いかがでしたか?

後半は息絶えてますが、こんな感じでNSを立てると冒頭に触れた機能を持ったサーバーができます。Dockerfileもそのまま使えるかは怪しいのであまり役に立たない記事ですが、あなたの鯖立てに少しでも貢献できていたら嬉しいです。

さいごに、1週間の大遅刻となってしまいすみませんでした……。

それではよいSimutransライフを!