ラズパイでテラリアのサーバーとbotを構築したので自分用メモ

# 前書き

友人をTerrariaの沼に沈め、また、自身もTerrariaに囚われてしまった責任を取ってサーバーを立てることにしたので、その際の作業をメモしておきます。

# 構成

* PC：Raspberry Pi 4 Model B/4GB element14
* OS:Ubuntu 23.04
* ストレージ:microSD(32GB)

自分のメインPCをずっと起動しておくのは憚られたので、消費電力の低さを買ってラズパイを利用することにしました。
ただ、Ubuntuのほうが記事もパッケージも多いので、OSはUbuntu Desktop 23.04を選択しました。GUI操作が欲しかったのでUbuntu ServerではなくUbuntu Desktopにしています。

# OSのインストール

microSDを差し込めるPCに[公式サイト内の配布ページ](https://www.raspberrypi.com/software/)からダウンロードしたRaspberry Pi Imagerをインストール。OSとしてUbuntuを、インストールディスクとしてmicroSDを選択してインストールします。
microSDのフォーマットはexFATではなくFAT32でなくてはならないことに注意しましょう。


# 起動後の設定

とりあえず`$ sudo apt update`と`$ sudo apt upgrade`を実行。

## 個人用の設定

必須ではないですが自分がいつもする設定も載せておきます。

* 電源プランを変更
設定アプリの"Power"から"Power Saving Option"の"Screen Blank"を"Never"に

* vimのインストールと設定
`$ sudo apt install vim`
`$ vim ~/.vimrc`
`$ source ~/.vimrc `

```.vimrc
set backspace=indent,eol,start                                                                      set ambiwidth=double
set ignorecase
set smartcase
set wrapscan
set incsearch
set hlsearch
set noerrorbells
set shellslash
set showmatch matchtime=1
set cinoptions+=:0
set cmdheight=2
set laststatus=2
set history=10000
set expandtab
set shiftwidth=2
set softtabstop=2
set tabstop=2
set guioptions+=a
set showmatch
set smartindent
set title
set number
set clipboard=unnamed,autoselect
```

* GNU Screenのインストールと設定
後々sshでサーバーを管理するために導入
`sudo apt install screen`
`vim .screenrc`

```.screenrc
autodetach on
deflog off
logfile "/home/$USER/screen-%Y%m%d-%n.log"
logfile flush secs
hardcopydir "/home/$USER/screen-hardcopy.%n"
defutf8 on
defencoding utf8
encoding utf8 utf8
hardstatus alwayslastline "%{= rw} %H %{= wk}%-Lw%{= bw}%n%f* %t%{= wk}%+Lw %{= wk}%=%{= gk} %y/%m/%d %c "
```

# ローカルIPアドレスの固定

実際にサーバーを立てたとして、自分は別のPCからローカルIP経由でサーバーに参加することになります。
そのため、ローカルIPアドレスは固定しておきたい。

やり方は主に[こちらの記事](https://office54.net/iot/linux/ubuntu-ipaddress-netplan)を参考にさせて頂きました。

まずはnet-toolsのインストール
`$ sudo apt install net-tools`

次に、ネット接続を確認
`$ ip -br -c addr show`
（ここでネットワークインターフェースも確認しておきます。）

そして、設定ファイルの編集に入ります。
デフォルトの設定ファイルをコピーし、元ファイルの拡張子を変えておきます。
`$ cp /etc/netplan/01-use-network-manager.yaml /etc/netplan/99-eth0-init.yaml`
`$ mv /etc/netplan/01-use-network-manager.yaml_old`

そして、`99-eth0-init.yaml`をvimで編集していきます。

```99-eth0-init.yaml
network:
   version: 2
   renderer: networkd
   ethernets:
      eth0:
         dhcp4: false
         dhcp6: false
         addresses: [192.168.10.220/24]
         nameservers:
            addresses: [192.168.10.1]
         routes:
          - to: default
            via: 192.168.10.1
```

インデントが大事なファイルなので、そこは注意しておきましょう。インデントエラーが出まくる場合は、expandコマンドでTab文字とスペースの置換を試してみてください。
（自分は version: 2 の「:」の後ろにあるスペースを忘れてエラーが出ていました。）

保存できたら、設定を反映します。
`$ sudo netplan apply`

もう一度`$ ip -br -c addr show`を試して、設定したIPになっていればOKです。

# ポート開放

Terrariaのサーバーアプリは自動でポートを開けてくれるはずですが、一応手動で開放します。
[こちらの記事](https://qiita.com/siida36/items/be21d361cf80d664859c)を主に参考にさせて頂きました。

まずはファイアウォールを有効化
`$ sudo ufw enable`

次に開放するポート(Terrariaのデフォルトは7777)を指定
`$ sudo ufw allow 7777`

最後に`$ sudo ufw reload`で適用、`$ sudo ufw status`で確認します。
環境によってはルーターから設定する必要がある場合もあるかも？

以降、sshやsambaのように外部と通信をする際には適宜ポートを開放する必要があることを忘れずに。

これで準備できたので、次からサーバーアプリを入れていきます。

# Terrariaサーバーを立てる

当初は[こちらの記事](https://vpslife.server-memo.net/ubuntu_terraria_install/)に従って[公式のアプリ](https://terraria.fandom.com/wiki/Server#Downloads)を使用する予定だったのですが、`TerrariaServer.bin.x86_64`を実行したところ`syntax error: word unexpected (expecting ")")`や`ELF not found`といったエラーに阻まれ、サーバーを立てられませんでした。
ラズパイはCPUがARMなので、それが原因なんじゃないかと思います。

途方に暮れていたところ、[ラズパイでTerrariaサーバーを立てている記事](https://qiita.com/Freedom645/items/6a3326956c4b0c739354)を発見！
非公式サーバークライアントの[TShock](https://github.com/Pryaxis/TShock/releases)なら問題なく動くとのことで、こちらを利用することに。

まずはzip解凍のためにunzipをインストール
`$ sudo apt install unzip`

サーバープログラムの実行に必要だそうなので、monoをインストール(要らなかったかも...？)
`$ apt-get install mono-complete`

TShockの実行に必要と言われたので .Net 6をインストール
`$ sudo apt install dotnet6`

セキュリティの関係でサーバー実行用のユーザーを用意したほうが良いとのことで、ユーザーを作成（今回は安直にterrariaという名前に）
`$ sudo adduser terraria`

ユーザーの切り替え
`$ su - terraria`

テラリアのサーバーを置くディレクトリを作成
`$ mkdir terraria`
`$ cd terraria`

これで事前の準備が済んだので、TShockをダウンロードしていきます。

[公式の配布ページ](https://github.com/Pryaxis/TShock/releases)から最新版のリンクをコピーして、ダウンロード
`$ curl -LO https://github.com/Pryaxis/TShock/releases/download/v5.2.0/TShock-5.2-for-Terraria-1.4.4.9-linux-arm64-Release.zip`

解凍していきます
`$ unzip TShock-5.2-for-Terraria-1.4.4.9-linux-arm64-Release.zip`
`$ tar -xvf TShock-Beta-linux-arm64-Release.tar`

あとは`$ ./TShock.Server`でサーバーを実行し、案内に従ってワールドの作成・選択やポート・パスワードの設定をすればOKです！

## 追加の設定

* 既にワールドデータが別PCにある場合は、`/home/$USER/.local/share/Terraria/Worlds/`に.wldファイルを置けば認識されます。

* `tshock/config.json`を弄ることでサーバーの設定を変更できます。（[こちらの記事](https://owen1924616.hateblo.jp/entry/ar587520)を参考にさせて頂きました。）
デフォルトでは`"SpawnProtection"`がtrueになっていたため、拠点の整備がしづらかったです。
気の知れた友人とやる前提なら、falseで良いので変更しました。

# botの作成

他の人を招くには、グローバルIPを相手に知らせる必要があります。しかし、これを固定するのはお金がかかります。
要は現在のグローバルIPさえ伝わればいいのですから、discordのbotを作って、コマンドを叩けばグローバルIPを返すようにすればいいのではと思い、botを作ることに。
（かなりの素人考えなので、セキュリティの問題を無視しまくっています。自己責任で。）

自分のグローバルIPを知るには、以下のコマンドを叩けばOKです。
`$ curl ifconfig.co`

botについては、書き慣れたpythonで書くことに。そのため、discord.pyをインストールします。
`$ pip install discord.py==1.7.3`
バージョンを最新のものにすると過去のコードが使えなかったので、過去のバージョンを指定しました（またしてもセキュリティ軽視）。

しかし、ここで`externally-managed-environment`というエラーが発生。
[こちらの記事](http://pineplanter.moo.jp/non-it-salaryman/2023/05/05/pip-error/)を参考に、`~/.pip/pip.conf`を変更して対処しました。

```pip.conf
[global]
break-system-packages = true
```

discordのbot作成のため、[discord公式の開発者サイト](https://discord.com/developers/applications)にてbotを作成し、トークンの取得とサーバーへの導入を済ませます。

あとはちゃっとコードを書いて、実行！

```terrabot.py
import asyncio
import discord
from discord.ext import commands
import subprocess

bot = commands.Bot(command_prefix="!")

token = "xxxxxxxxxxxxxxxxxxxxxxxxxxx"

#botの読み込み直後に実行
@bot.event
async def on_ready():
    print('My bot ready!')
    print('discord.py version is ' + discord.__version__)
    adress = subprocess.run(["curl","ifconfig.co"],shell=True,text=True,stdout=subprocess.PIPE).stdout
    activity = discord.Activity(name=adress, type=discord.ActivityType.playing)
    await bot.change_presence(activity=activity)

#関数作成
@bot.command()
async def terraria(ctx):
    """IPアドレスを表示"""
    adress = subprocess.run(["curl","ifconfig.co"],shell=True,text=True,stdout=subprocess.PIPE).stdout
    await ctx.send(adress)
    activity = discord.Activity(name=adress, type=discord.ActivityType.playing)
    await bot.change_presence(activity=activity)

bot.run(token)
```

毎回コマンドを叩いてもらわなくても済むように、アクティビティにIPを表示しておきます。
ただ、念のためにコマンドも用意し、コマンドが叩かれたらアクティビティも更新するようにしました。

## 更なる機能拡張

botを作ったとなると、botから現在のプレイヤー数が分かれば便利だなあと感じ、実装することに。
当初はGNU screenに命令を送ってログを読むという力技で解決しようとしていましたが、なんとTShockにはAPIがあるようです！（[公式サイト](https://tshock.readme.io/v4.3.22/reference/rest-api-endpoints)）

というわけで、これを使いましょう。

まずは一旦サーバーを止めて、`tshock/config.json`を編集します。

`"RestApiEnabled"`をtrueに変更し、`"RestApiPort": 7878`となっていることを確認します。

APIの通信に7878ポートを使うので、`$ sudo ufw allow 7878`で7878ポートを開放します。
`$ sudo ufw reload`で適用し、`$ sudo ufw status`で確認しました。

設定出来たらサーバーを起動し、[こちらのサイト](https://terraria.arcenserv.info/wiki/TShock_%E5%85%AC%E5%BC%8Fwiki%E3%81%AE%E5%92%8C%E8%A8%B3
)を参考にsuperadminユーザーを作成します。
`/user add aoi hoge superadmin`
これで、aoiというユーザーをhogeというパスワードでsuperadminとして登録できました。

では、[公式サイト](https://tshock.readme.io/v4.3.22/reference/rest-api-endpoints)を参考にまずはAPIの利用に必要なトークンを取得しましょう。

以下のURLにアクセスします。
http://127.0.0.1:7878/v2/token/create?username=aoi&password=hoge

サーバーを実行しているラズパイからアクセスするなら127.0.0.1:7878で問題ないはずですが、別PCからアクセスするならここはラズパイのローカルIPに変更しましょう。
また、aoi, hoge は自分が設定したsuperadminアカウントのユーザー名とパスワードに変更してください。

トークンさえ取得できたら、[http://127.0.0.1:7878/lists/players?token=xxxxxxxxxxxxxxxxxxxxx](http://127.0.0.1:7878/lists/players?token=xxxxxxxxxxxxxxxxxxxxx)から現在の参加プレイヤーを確認できます。

後はこの情報をdiscordのbotで扱えるようにすればOKです。

以下の関数を付け加えればOK。

```terrabot.py
import requests
import json

@bot.command()
async def player(ctx):
    """プレイ中の人数を表示"""
    url = requests.get("http://127.0.0.1:7878/lists/players?token=xxxxxxxxxxxxxxxxxxxxx")
    text = url.text
    data = json.loads(text)
    player_list = data["players"].split(",")
    print(player_list)
    if player_list[0] == "":
        await ctx.send("現在0人が遊んでいます")
    else:
        await ctx.send(f"現在{len(player_list)}人が遊んでいます")
```

情報がjson形式で送られてくるので、辞書型を思い出しながら実装。
プレイヤーが0人でも空文字列が返ってくるので、人数を返す際に例外処理をしています。

### 追記

サーバーを立て直したらトークンが無効になることを失念していました。
そのため、正しくは以下のような形になります。

```terrabot.py
import requests
import json

@bot.command()
async def player(ctx):
    """プレイ中の人数を表示"""
    token_url = requests.get("http://127.0.0.1:7878/v2/token/create?username=aoi&password=hoge")
    token_text = token_url.text
    token_data = json.loads(token_text)
    terra_token = token_data["token"]
    url = requests.get(f"http://127.0.0.1:7878/lists/players?token={terra_token}")
    text = url.text
    data = json.loads(text)
    player_list = data["players"].split(",")
    print(player_list)
    if player_list[0] == "":
        await ctx.send("現在0人が遊んでいます")
    else:
        await ctx.send(f"現在{len(player_list)}人が遊んでいます")
```

bot起動中にサーバーを立て直した場合を考え、関数が実行されるたびにtokenを取得する形にしていますが、短時間に何度も関数を呼び出すと固まってしまいそうです...痛しかゆし...。

サーバーを立て直すたびにbotを再起動するなら、global変数にした方がいいですね。

# 総括
これにてやりたかったことが全て実現出来ました！
やろうと思えばdiscordから様々な命令が送れるようにもできますが、まあ、流石にこれはやめておきましょう。

sshやsambaの設定についてもまた記事を書くかもしれません。

では。