okayurisotto.net

私が好きでやったことが他の人のためにもなったらお得かも!

MinecraftのblockstateファイルのJSON構造が気に食わないので、代替構造を作った

  1. 📝
  2. 🔄

はじめに

実際、リソースパックの仕組みには無理があります。

しばらく前に書いた、blockstateファイルに対する愚痴記事より

「Mojangなんとかしろ」案件なのですがなんともならなそうなので、自力でなんとかすることにしました。

何をなんとかしたいのか

Minecraft Java Editionではリソースパックによってゲーム内のアイテムやブロックの見た目をある程度自由に改変できます。ブロックの見た目の決定では、ブロックが持つ状態(blockstate)に応じてモデル(model)を選ぶようになっていますが、これに関してもリソースパックでの改変が可能です。しかしながらその改変はとてつもなく面倒なものとなってしまっています。というのも、ブロックの状態に応じたモデルの定義はブロック毎に用意されたJSONファイルで行うのですが、このJSONの構造が非力で、複雑なことをしようとすると途端に面倒になってしまうのです。

例を挙げましょう。Minecraftには1.20から、Pink Petalsという装飾ブロックが追加されます。これはCherry Groveバイオームに生成されるもので、「桜の絨毯」っぽいものの厳密にはそうではない植物です。このブロックには東西南北の方向と、4つの成長段階があります。つまり16通りの見た目を取ります。これは次のようなブロック状態ファイル(blockstates/pink_petals.json)によって定義されています。

{
  "multipart": [
    {
      "apply": { "model": "minecraft:block/pink_petals_1" },
      "when": { "facing": "north", "flower_amount": "1|2|3|4" }
    },
    {
      "apply": { "model": "minecraft:block/pink_petals_1", "y": 90 },
      "when": { "facing": "east", "flower_amount": "1|2|3|4" }
    },
    {
      "apply": { "model": "minecraft:block/pink_petals_1", "y": 180 },
      "when": { "facing": "south", "flower_amount": "1|2|3|4" }
    },
    {
      "apply": { "model": "minecraft:block/pink_petals_1", "y": 270 },
      "when": { "facing": "west", "flower_amount": "1|2|3|4" }
    },

    {
      "apply": { "model": "minecraft:block/pink_petals_2" },
      "when": { "facing": "north", "flower_amount": "2|3|4" }
    },
    // 以下略
  ]
}

16通りの状態をそれぞれ定義し、multipartで同じ座標に重ねるようなことをしています。……面倒じゃありませんか? 「4つの成長段階について事前に定義した1つのオブジェクト」と「それを参照しつつ4つの方向を定義する4つのオブジェクト」というような書き方はできないのでしょうか? ……残念ながらできません。どうやらMojangはこういった需要を見越した仕様にはしていないようです。

自力でなんとかする

であれば自力でなんとかするまでです。そのJSONファイルを直接編集するのではなく、書きやすいオレオレ構造なJSONをプログラムで変換するようにしてしまえばいいのです。

そうしてできたのがgithub.com/okayurisotto/alt-blockstateです。

使っている技術

私が唯一扱える言語であるTypeScriptを今回も使うために、Denoにご登場頂きました。コマンドラインからうまく使えるように、Cliffyライブラリにもご協力頂きました。

オレオレJSON構造について

JSONファイルの編集の際にエディタ補完がされるようにJSON Schemaを書きましたので、JSON Schemaを読める人は/schema.jsonを読んでください。また、/samples/*.jsonが例です。例を読むだけでなんとなく雰囲気はわかるかもしれません。TypeScriptでの定義は/types.tsにあります。

簡単にまとめると、「事前定義したオブジェクトを参照する機能」と「モデル全体の回転などについての定義を継承する機能」によって、書きやすくメンテナンスしやすい構造が実現されました。

オレオレJSON構造には、6種類のモデル定義オブジェクト形式があります。

1つ目はrootオブジェクトです。このオブジェクトのmodelプロパティでそのブロックに対するモデルを記述します。refsプロパティも持ち、これは参照機能で使われます。

2つ目はrefオブジェクトです。このオブジェクトはrefプロパティで、事前にrootオブジェクトのrefsプロパティで定義していたオブジェクトを参照します。refオブジェクトがさらに別のオブジェクトを参照することも可能な作りになっていると思われますが試していません。

3つ目はnormalオブジェクトです。このオブジェクトが、modelプロパティによってモデルファイルへのリンク文字列を持ちます。

4つ目はconditionオブジェクトです。このオブジェクトはwhenプロパティとmodelプロパティを持ちます。役割としては特に目新しいものはありません。従来のJSON構造のwhenと同じです。

5つ目はallOfオブジェクトです。このオブジェクトを使うと、modelsプロパティで指定した複数のモデル定義オブジェクトすべてを適用することができます。

6つ目はoneOfオブジェクトです。これは土ブロックなどでやっているような「複数のモデルからランダムに選ぶ」というような動作をするときに使うものです。

これらを組み合わせることで、うまくモデルを定義していきます。

具体例

pink_petals.jsonを例に、実際の定義方法についてまとめます。

{
  "type": "root",
  "options": { "y": { "type": "+", "value": 90 } }, // 回転の基準となる値を事前に定義しておく(初期値0に90を足すことで90を定義)
  "model": {
    "type": "allOf", // すべてを適用するが実際に使われるかは状態次第
    "models": [
      // conditionオブジェクトを使ってそれぞれの方角のモデル(計4つ)を定義する
      {
        "type": "condition",
        "when": { "facing": ["north"] },
        "options": { "y": { "type": "*", "value": 0 } }, // 事前定義した値とかけ算をすることで、具体的にどの程度回転されるのかが求まる
        "model": { "type": "ref", "ref": "flower_amount" } // 参照している
      },
      {
        "type": "condition",
        "when": { "facing": ["east"] },
        "options": { "y": { "type": "*", "value": 1 } }, // ここは90
        "model": { "type": "ref", "ref": "flower_amount" }
      },
      {
        "type": "condition",
        "when": { "facing": ["south"] },
        "options": { "y": { "type": "*", "value": 2 } }, // ここは180
        "model": { "type": "ref", "ref": "flower_amount" }
      },
      {
        "type": "condition",
        "when": { "facing": ["west"] },
        "options": { "y": { "type": "*", "value": 3 } }, // ここは270
        "model": { "type": "ref", "ref": "flower_amount" }
      }
    ]
  },
  "refs": {
    "flower_amount": { // これが参照される
      "type": "allOf", // すべてを適用するが実際に使われるかは状態次第
      "models": [
        {
          "type": "condition",
          "when": { "flower_amount": ["1", "2", "3", "4"] }, // 配列であることに注意
          "model": {
            "type": "normal",
            "model": "minecraft:block/pink_petals_1"
          }
        },
        {
          "type": "condition",
          "when": { "flower_amount": ["2", "3", "4"] },
          "model": {
            "type": "normal",
            "model": "minecraft:block/pink_petals_2"
          }
        },
        {
          "type": "condition",
          "when": { "flower_amount": ["3", "4"] },
          "model": {
            "type": "normal",
            "model": "minecraft:block/pink_petals_3"
          }
        },
        {
          "type": "condition",
          "when": { "flower_amount": ["4"] },
          "model": {
            "type": "normal",
            "model": "minecraft:block/pink_petals_4"
          }
        }
      ]
    }
  }
}

この他にも、モデルを名前で参照するときに使えるテンプレート文字列機能(variables)があります。詳しくはsamples/oak_door.jsonを見てください。

使い方

Denoをインストールした上で、下のように実行すると、従来のJSON構造に変換されて標準出力に出力されます。

deno run --allow-read main.ts path/to/file.json

おわりに

Mojangなんとかしなくていいよ。もう自力でなんとかしたから。