Pythonを使ってmcfunctionやjsonを生成しよう 〜カスタム武器作成の補助を例に〜

## 1. はじめに

本記事では、Minecraft Java Editionのデータパック製作において、Pythonを使ってmcfunctionやjsonを生成する方法を説明していきます。

:::note warn
この記事はMinecraft Java Edition ver.1.21.3 時点の情報を元に執筆しています。
:::

:::note warn
この記事はデータパックのfunction・loot_tableおよびプログラミングの基礎がわかる人向けのものです。
:::

データパックを作っているとき、同じような形式のmcfunction・jsonファイルを大量に作ることがあるかもしれません。カスタム武器、カスタムモブ、NPCとの会話、ショップの取引…こういった処理を沢山作るときとか。
こういった場合に、Pythonなどのプログラミング言語を用いてテンプレート生成プログラムを作っておくと大量作成が楽になります。

この記事は**プログラミング言語のPythonを使ったmcfunction・jsonファイルの生成方法**の一例を紹介していきます。

**<font color="red">※自身でも同じことを試したい場合、Pythonの実行環境を用意する必要があります。</font>**

## 2.カスタム武器を作る

この記事では、カスタム武器の作成を例に解説していきます。

例えば、とあるカスタム武器と、それを入手できるloot tableを以下のように実装します。
（ベースとなるアイテムを仮にニンジン付きの棒としています。）

カスタム武器：
![スクリーンショット 2024-11-02 11.42.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924924/1e2100c2-b9f0-2534-22a1-43654ba48e80.png)


<details><summary><font color="Blue">loot table のサンプル</font></summary>

```json
{
	"type": "minecraft:command",
	"pools": [
		{
			"rolls": 1,
			"entries": [
				{
					"type": "minecraft:item",
					"name": "minecraft:carrot_on_a_stick",
					"weight": 1,
					"functions": [
						{
							"function": "minecraft:set_name",
							"entity": "this",
							"name": {
								"text": "すごい鉄の斧",
								"color": "white",
								"italic": false
							}
						},
						{
							"function": "minecraft:set_lore",
							"entity": "this",
							"lore": [
								{
									"text": "  なんだかとてもすごいらしい斧。",
									"italic": false,
									"color": "yellow"
								},
								{
									"text": ""
								},
								{
									"text": "  攻撃力 +8",
									"italic": false,
									"color": "blue"
								},
								{
									"text": "  攻撃速度 0.8",
									"italic": false,
									"color": "blue"
								}
							],
							"mode": "replace_all"
						},
						{
							"function": "minecraft:set_attributes",
							"modifiers": [
								{
									"id": "weapon.attack_speed.0.8",
									"attribute": "minecraft:attack_speed",
									"amount": -3.2,
									"operation": "add_value",
									"slot": "mainhand"
								},
								{
									"id": "weapon.attack_damage.add.8",
									"attribute": "minecraft:attack_damage",
									"amount": 8,
									"operation": "add_value",
									"slot": "mainhand"
								}
							]
						},
						{
							"function": "minecraft:set_components",
							"components": {
								"minecraft:item_model": "minecraft:iron_axe"
							}
						},
						{
							"function": "minecraft:toggle_tooltips",
							"toggles": {
								"minecraft:attribute_modifiers": false
							}
						}
					]
				}
			]
		}
	]
}
```
</details>

このようなカスタム武器で決められる要素は以下の5つとします。
- 名前
- 説明文
- 攻撃力
- 攻撃速度
- 外見

では、「この5要素を列挙すれば、自動的にloot tableやfunctionを生成してくれるプログラム」をPythonで組んでいきます。

ディレクトリ構成は以下のようにしています。
```
<Datapack Name>/
├── pack.mcmeta
├── data/
│   └── <namespace>/
│       ├── function/
│       │   └── give/
│       │       └── <custom_weapon_id>.mcfunction
│       └── loot_table/
│           └── custom_weapon/
│               └── <custom_weapon_id>.json
└── python/
    ├── weapon_database.json
    └── generate_weapon_loot_table.py
```
例として今回は`pack.mcmeta`のあるディレクトリ下にpythonディレクトリを用意し、その場所にプログラムとデータベースファイルを作っていきます。

### 2-1. loot table 生成のサンプル

<details><summary><font color="Blue">データベースファイルのサンプル</font></summary>

```json:weapon_database.json
{
    "sugoi_iron_axe": {
        "display_name": "すごい鉄の斧",
        "lore": "なんだかとてもすごいらしい斧",
        "attack_damage": 8,
        "attack_speed": 0.8,
        "item": "iron_axe"
    },
    "hayahaya_golden_sword": {
        "display_name": "はやはやゴールデンソード",
        "lore": "とても速く振れる剣",
        "attack_damage": 1,
        "attack_speed": 10.0,
        "item": "golden_sword"
    }
}
```
</details>


<details><summary><font color="Blue">loot table 生成プログラムのサンプル</font></summary>

```python:generate_weapon_loot_table.py
import json
import os

# 指定パスのディレクトリが存在しなければ、ディレクトリを作成する関数を定義
def makedir(path):
	if not os.path.isdir(os.path.dirname(path)):
		os.makedirs(os.path.dirname(path))

# データベースファイルの読み込み
with open('weapon_database.json' , encoding='utf-8') as f:
	weapon_database = json.load(f)

# データパック内の名前空間への相対パス
namespace_path = '../data/sample_namespace/'

# 各武器に対する処理
for weapon_id, weapon_data in weapon_database.items():

	# 書き込み先のパス指定
	path = namespace_path + 'loot_table/custom_weapon/' + weapon_id + '.json'

	# 書き込み先のディレクトリの作成
	makedir(path)

	# 出力内容を辞書形式で定義
	output = {
		"type": "minecraft:command",
		"pools": [
			{
				"rolls": 1,
				"entries": [
					{
						"type": "minecraft:item",
						"name": "minecraft:carrot_on_a_stick",
						"weight": 1,
						"functions": [
							{
								"function": "minecraft:set_name",
								"entity": "this",
								"name": {
									"text": weapon_data["display_name"],
									"color": "white",
									"italic": False
								}
							},
							{
								"function": "minecraft:set_lore",
								"entity": "this",
								"lore": [
									{
										"text": "  " + weapon_data["lore"],
										"italic": False,
										"color": "yellow"
									},
									{
										"text": ""
									},
									{
										"text": "  攻撃力 +" + str(weapon_data["attack_damage"]),
										"italic": False,
										"color": "blue"
									},
									{
										"text": "  攻撃速度 " + str(weapon_data["attack_speed"]),
										"italic": False,
										"color": "blue"
									}
								],
								"mode": "replace_all"
							},
							{
								"function": "minecraft:set_attributes",
								"modifiers": [
									{
										"id": "weapon.attack_damage.add." + str(weapon_data["attack_damage"]),
										"attribute": "minecraft:attack_damage",
										"amount": weapon_data["attack_damage"],
										"operation": "add_value",
										"slot": "mainhand"
									},
									{
										"id": "weapon.attack_speed." + str(weapon_data["attack_speed"]),
										"attribute": "minecraft:attack_speed",
										"amount": weapon_data["attack_speed"]-4.0,
										"operation": "add_value",
										"slot": "mainhand"
									}
								]
							},
							{
								"function": "minecraft:set_components",
								"components": {
									"minecraft:item_model": weapon_data["item"]
								}
							},
							{
								"function": "minecraft:toggle_tooltips",
								"toggles": {
									"minecraft:attribute_modifiers": False
								}
							}
						]
					}
				]
			}
		]
	}
	
	# jsonファイルの書き込み
	with open(path, 'w', encoding='utf-8') as f:
		json.dump(output, f, indent='\t', ensure_ascii=False)

```
</details>

サンプルプログラムを実行すると、データパックの`loot_table`ディレクトリ内にカスタム武器のjsonファイルが生成されます。


### 2-2. loot table 生成プログラムの解説

loot table 生成プログラムの中身を少し解説します。

全体で行っていること：
- データベースファイル（各カスタム武器の要素を列挙したファイル）の中身を読み込み、変数に格納する
- 各武器の要素を取り出して以下を実行：
    - 書き込み先のファイルパスを指定（=どこにどういう名前のファイルを生成するか指定）する
    - 書き込むloot table（jsonファイル）の中身を作る
    - jsonファイルに書き込む


<details><summary><font color="Blue">データベースファイルの読み込み、変数への格納</font></summary>

### データベースファイルの読み込み、変数への格納
今回のサンプルでは、データベースファイル（各カスタム武器の要素を列挙したファイル）をjson形式で作成しています。
```python
# データベースファイルの読み込み
with open('weapon_database.json' , encoding='utf-8') as f:
	weapon_database = json.load(f)
```

`'weapon_database.json'`の部分で、データベースファイルのパスを指定します。
`weapon_database = json.load(f)`の部分で、読み込んだデータの中身を変数`weapon_database`に入れます。

</details>


<details><summary><font color="Blue">列挙した各武器の要素を取り出す</font></summary>

### 列挙した各武器の要素を取り出す
サンプルプログラム内では、データベースファイルで列挙した各武器データの要素を取り出して個別の処理を行っています。
```python
# 各武器に対する処理
for weapon_id, weapon_data in weapon_database.items():
    /* 各武器に対する個別の処理 */
```

`weapon_database`には辞書形式でデータが列挙されています。（辞書形式＝Pythonでのjsonフォーマットみたいなデータ形式）
辞書形式はデータのidと中身の組を列挙するような方式です。データのidに重複が許されないという制約に注意してください。
上のような`for`文を書けば、そのidと中身をそれぞれ変数`weapon_id`, `weapon_data`に格納することができます。

</details>


<details><summary><font color="Blue">書き込むloot table（jsonファイル）の中身を指定</font></summary>

### 書き込むloot table（jsonファイル）の中身を指定

以下のように書き込むloot table（jsonファイル）の中身を指定しています。

```python

# 出力内容を辞書形式で定義
output = {
    "type": "minecraft:command",
    "pools": [
        {
            "rolls": 1,
            "entries": [
                {
                    "type": "minecraft:item",
                    "name": "minecraft:carrot_on_a_stick",
                    "weight": 1,
                    "functions": [
                        {
                            "function": "minecraft:set_name",
                            "entity": "this",
                            "name": {
                                "text": weapon_data["display_name"],
                                "color": "white",
                                "italic": False
                            }
                        },
                        /* 略 */
                    ]
                }
            ]
        }
    ]
}
```

`weapon_data`にはカスタム武器の個別データが格納されており、`weapon_data["display_name"]`でその武器の表示名を取り出しています。

json形式では`true`、`false`は小文字ですが、Pythonでは`True`、`False`のように先頭を大文字にする必要があります。

</details>


<details><summary><font color="Blue">jsonファイルの書き込み</font></summary>

### jsonファイルの書き込み

以下のようにjsonファイルを書き込みます。

```python

# jsonファイルの書き込み
with open(path, 'w', encoding='utf-8') as f:
    json.dump(output, f, indent='\t', ensure_ascii=False)

```

`path`の場所にはファイルを書き込む相対パス、`output`の場所には書き込むデータの中身を指定します。

</details>


### 2-3. mcfunction 生成のサンプルと簡単な解説

上で生成したloot tableを呼び出すだけの簡単なmcfunctionを生成してみます。

<details><summary><font color="Blue">mcfunction 生成プログラムのサンプル</font></summary>

```python:genarate_weapon_command.py
import json
import os

# 指定パスのディレクトリが存在しなければ、ディレクトリを作成する関数を定義
def makedir(path):
	if not os.path.isdir(os.path.dirname(path)):
		os.makedirs(os.path.dirname(path))

# データベースファイルの読み込み
with open('weapon_database.json' , encoding='utf-8') as f:
	weapon_database = json.load(f)

# データパックの名前空間
datapack_namespace = 'sample_namespace'

# データパック内の名前空間への相対パス
namespace_path = '../data/' + datapack_namespace + '/'

# 各武器に対する処理
for weapon_id, weapon_data in weapon_database.items():

	# 書き込み先のパス指定
    path = namespace_path + 'function/give/' + weapon_id + '.mcfunction'

	# 書き込み先のディレクトリの作成
    makedir(path)

    # 書き込むファイルの中身を指定
    output = []
    output.append('loot give @s loot '+ datapack_namespace + ':custom_weapon/' + weapon_id + '\n')

    # テキストファイルの書き込み
    with open(path, 'w', encoding='utf-8') as f:
        f.writelines(output)

```

</details>

テキストファイル書き込みの際には`writelines()`関数が便利です。

リスト形式で文字列データを並べると、まとめてファイルに書き込みをしてくれます。
ただし自動で改行はしてくれないので、複数行のコマンドを記述する際には改行文字`'\n'`を自分で書く必要があります。

`output = []`で空のリストを用意し、
`output.append('loot give @s loot '+ datapack_namespace + ':custom_weapon/' + weapon_id + '\n')`のように1行ずつ追加していくイメージです。


## 2-4. データベースの形式について

サンプルでは **json** 形式でデータベースファイルを作っていましたが、 **csv** 形式も便利です。
csvはコンマと改行を使って文字列を区切る形式です。excelのようなテーブルと相性がよく、excelシートをcsv形式で保存することもできます。VSCodeの拡張機能にcsvをテーブル形式で表示・編集できるものもあります。

また、GoogleのAPIを使って、PythonからGoogleスプレッドシートを読み込むことも可能みたいです。
自分に合ったやり方を選んでみてください。

## 3. さいごに

このファイル生成が真価を発揮するのは、大規模なデータパック/配布マップ製作だと思います。
RPGマップなどを製作する際には武器や防具、ショップやエリア移動など、様々な処理を大量に作る必要があります。
それを1種類追加するたびにコピペと編集を繰り返すのは疲れるので、今回紹介した方法を使ってテンプレートを用意しておけば、（最初に頑張る必要はありますが）あとで楽できるようになります。製作中に仕様を変更したい場合にも対応が楽になるので、長期的な開発にはおすすめする方法です。

***それでは、良きコマンドライフを！***