[Minecraft 1.12.2] バニラの鉱石生成（WorldGenMinable）と鉱石生成の追加方法

解析情報という名のMinecraft攻略記事

# 環境

- forgeBin-1.12.2-14.23.5.2768.jar

# バニラの鉱石生成

## 見る場所

地上の鉱石生成部分のコード。

- `void net.minecraft.world.gen.ChunkGeneratorOverworld.populate(int p_populate_1_, int p_populate_2_)`
- `void net.minecraft.world.biome.Biome.decorate(World p_decorate_1_, Random p_decorate_2_, BlockPos p_decorate_3_)`
- `void net.minecraft.world.biome.BiomeDecorator.decorate(World p_decorate_1_, Random p_decorate_2_, Biome p_decorate_3_, BlockPos p_decorate_4_)`
- `void net.minecraft.world.biome.BiomeDecorator.genDecorations(Biome p_genDecorations_1_, World p_genDecorations_2_, Random p_genDecorations_3_)`
- `void net.minecraft.world.biome.BiomeDecorator.generateOres(World p_generateOres_1_, Random p_generateOres_2_)`

ネザーの鉱石生成部分のコード。

- `void net.minecraft.world.gen.ChunkGeneratorHell.populate(int p_populate_1_, int p_populate_2_)`

基本は`IChunkGenerator.populate`で鉱石が生成される。ネザーは当該メソッドに直接ネザークォーツ生成のコードが置かれているが、地上は地形生成が複雑なのでメソッドを4個呼んだ先で鉱石が生成されている。

## 鉱石の生成場所算出の基本的なルーチン

例としてネザークォーツを生成するコードを整形して見てみる。

```java:ChunkGeneratorHell#populate内
WorldGenerator quartzGen = new WorldGenMinable(
 Blocks.QUARTZ_ORE.getDefaultState(), // oreBlock 置換後のブロックを直接指定する
 14, // numberOfBlocks 数が大きいほど多くの鉱石が生成されるが、14回生成判定が行われる訳ではない模様
 BlockMatcher.forBlock(Blocks.NETHERRACK) // predicate 置換前のブロックを識別する判定機
 );
BlockPos blockpos = new BlockPos(chunkX * 16, 0, chunkZ * 16);

if (TerrainGen.generateOre(world, rand, quartzGen, blockpos, OreGenEvent.GenerateMinable.EventType.QUARTZ)) {
 for (int i = 0; i < 16; i++) {
  quartzGen.generate(world, rand, blockpos.add(
   rand.nextInt(16),
   rand.nextInt(108) + 10,
   rand.nextInt(16)));
 }
}
```

`quartzGen`は不変である。ifの中身はネザーにネザークォーツを生成するか否かの判定で、MODが`OreGenEvent.GenerateMinable`イベントをキャッチして`result`に`DENY`をセットするとキャンセルされる機構になっている。本質でないのでこの判定部分を割愛するとこうなる。

```java:ChunkGeneratorHell#populate内
WorldGenerator quartzGen = new WorldGenMinable(
 Blocks.QUARTZ_ORE.getDefaultState(), // oreBlock 置換後のブロックを直接指定する
 14, // numberOfBlocks 数が大きいほど多くの鉱石が生成されるが、14回生成判定が行われる訳ではない模様
 BlockMatcher.forBlock(Blocks.NETHERRACK) // predicate 置換前のブロックを識別する判定機
 );
BlockPos blockpos = new BlockPos(chunkX * 16, 0, chunkZ * 16);

for (int i = 0; i < 16; i++) {
 quartzGen.generate(world, rand, blockpos.add(
  rand.nextInt(16),
  rand.nextInt(108) + 10,
  rand.nextInt(16)));
}
```

----

`rand.nextInt(16)`は0～15の範囲の整数乱数、`rand.nextInt(108) + 10`は10～117の範囲の整数乱数を与える。`blockpos`はそのチャンクの北西のブロックを表し、X・Z座標はともに16の倍数である。16の倍数に0～15の範囲の整数乱数を加えるので、XZ座標は「そのチャンク内のランダムなどこか」となる。一方Y座標は単に10～117である。それを1チャンク当たり固定で16回試行するので、完全にネザーラックで埋まったチャンクには、**XZ座標は均一に、Y座標は10～117を起点とした範囲に、合計16個の、大きさ14のネザークォーツの鉱脈**があることになる。

## バニラの鉱石生成が記述されている場所

ネザークォーツを生成する`WorldGenerator`は`WorldGenMinable`であったが、これは地上側でもメソッドの呼び出し過程が違うだけで同じように呼び出されている。

`BiomeDecorator#decorate`中の`WorldGenMinable`の呼び出し場所は次のとおりである。

```java:BiomeDecorator#decorate内
dirtGen      = new WorldGenMinable(Blocks.DIRT.getDefaultState(), chunkProviderSettings.dirtSize);
gravelOreGen = new WorldGenMinable(Blocks.GRAVEL.getDefaultState(), chunkProviderSettings.gravelSize);
graniteGen   = new WorldGenMinable(Blocks.STONE.getDefaultState().withProperty(BlockStone.VARIANT, BlockStone.EnumType.GRANITE), chunkProviderSettings.graniteSize);
dioriteGen   = new WorldGenMinable(Blocks.STONE.getDefaultState().withProperty(BlockStone.VARIANT, BlockStone.EnumType.DIORITE), chunkProviderSettings.dioriteSize);
andesiteGen  = new WorldGenMinable(Blocks.STONE.getDefaultState().withProperty(BlockStone.VARIANT, BlockStone.EnumType.ANDESITE), chunkProviderSettings.andesiteSize);
coalGen      = new WorldGenMinable(Blocks.COAL_ORE.getDefaultState(), chunkProviderSettings.coalSize);
ironGen      = new WorldGenMinable(Blocks.IRON_ORE.getDefaultState(), chunkProviderSettings.ironSize);
goldGen      = new WorldGenMinable(Blocks.GOLD_ORE.getDefaultState(), chunkProviderSettings.goldSize);
redstoneGen  = new WorldGenMinable(Blocks.REDSTONE_ORE.getDefaultState(), chunkProviderSettings.redstoneSize);
diamondGen   = new WorldGenMinable(Blocks.DIAMOND_ORE.getDefaultState(), chunkProviderSettings.diamondSize);
lapisGen     = new WorldGenMinable(Blocks.LAPIS_ORE.getDefaultState(), chunkProviderSettings.lapisSize);
```

見ての通り、鉱石系ブロック以外に土や岩石も`WorldGenMinable`であることが分かる。

この`WorldGenerator`の`generate`を呼び出すと鉱石が生成されるわけだが、呼び出され方が2種類ある。一つは通常のもので、もう一つはラピスラズリ専用の方法である。

```java:BiomeDecorator#generateOres内
// ラピスラズリ以外（例：ダイヤモンド）
genStandardOre1(world, random, chunkProviderSettings.diamondCount, diamondGen,
 chunkProviderSettings.diamondMinHeight,
 chunkProviderSettings.diamondMaxHeight);

void genStandardOre1(World world, Random random, int count, WorldGenerator worldGen, int minHeight, int maxHeight)
{

 if (maxHeight < minHeight) { // minの方が大きかったら入れ替える
  int i = minHeight;
  minHeight = maxHeight;
  maxHeight = i;
 } else if (maxHeight == minHeight) { // min == maxだったら、生成範囲を拡張する
  if (minHeight < 255) {
   maxHeight++;
  } else {
   minHeight--;
  }
 }
 // この時点でmin < maxが確定

 for (int j = 0; j < count; ++j) {
  BlockPos blockpos = chunkPos.add(
   random.nextInt(16),
   random.nextInt(maxHeight - minHeight) + minHeight,
   random.nextInt(16));
  worldGen.generate(world, random, blockpos);
 }
}
```

```java:BiomeDecorator#generateOres内
// ラピスラズリ
genStandardOre2(world, random, chunkProviderSettings.lapisCount, lapisGen,
 chunkProviderSettings.lapisCenterHeight,
 chunkProviderSettings.lapisSpread);

void genStandardOre2(World world, Random random, int count, WorldGenerator worldGen, int centerHeight, int spread)
{
 for (int i = 0; i < count; ++i) {
  BlockPos blockpos = chunkPos.add(
   random.nextInt(16),
   random.nextInt(spread) + random.nextInt(spread) + centerHeight - spread,
   random.nextInt(16));
  worldGen.generate(world, random, blockpos);
 }
}
```

----

`genStandardOre1`の前半を占める引数の値チェック部分を無視すると、両者の違いは生成高度の算出のされ方のみである。ネザークォーツの生成も「ラピスラズリ以外」の生成ルーチンに一般化できる。`chunkPos`は、生成対象のチャンクの北西のブロック座標である。

まとめると、鉱石生成のYの算出方法は次のようになる。

- `rand.nextInt(maxHeight - minHeight) + minHeight`: 地上のラピスラズリ以外
- `rand.nextInt(spread) + rand.nextInt(spread) + centerHeight - spread`: ラピスラズリ
- `rand.nextInt(108) + 10`: ネザークォーツ

ネザークォーツは、「地上のラピスラズリ以外」で`minHeight = 10`，`maxHeight = 118`とした場合と同じである。

----

「地上のラピスラズリ以外」は`minHeight`～`maxHeight - 1`の範囲の整数乱数である。

一方、ラピスラズリの方は少し特殊である。`rand.nextInt(spread) + rand.nextInt(spread)`の部分は、例えば`spread = 3`とすると`[0 ~ 2] + [0 ~ 2] = [0, 1, 1, 2, 2, 2, 3, 3, 4]`という雰囲気になる（9個の数のうちランダムに得るの意）。最低値が0、最頻値が`spread - 1`、最大値が`(spread - 1) * 2`の三角型の確率分布である。これに`centerHeight - spread`を加えているので、最低値が`centerHeight - spread`、最頻値が`centerHeight - 1`、最大値が`centerHeight + spread - 2`の三角型の確率分布となる。

`centerHeight - 1`を中心として`±(spread - 1)`の範囲に分布するとみてよい。

## 鉱石生成のパラメータ

地上の鉱石生成のパラメータは`chunkProviderSettings`に支配されているが、これが供給する値の源流は`net.minecraft.world.gen.ChunkGeneratorSettings.Factory`にある。

まとめると以下のようになる。数値の左の`*`は、それがコンフィグで変えられないことを表す。列`g`は、1のときに汎用、2のときにラピスラズリ用の座標算出を行う。列`g`はコンフィグで変更することはできない。

```
name         g size count    minHeight maxHeight 備考
                          centerHeight    spread
dirt         1   33    10            0       256
gravel       1   33     8            0       256
granite      1   33    10            0        80
diorite      1   33    10            0        80
andesite     1   33    10            0        80
coal         1   17    20            0       128
iron         1    9    20            0        64
gold         1    9     2            0        32
gold         1    9   *20          *32       *80 メサのみ
redstone     1    8     8            0        16
diamond      1    8     1            0        16
lapis        2    7     1           16        16
netherQuartz 1  *14   *16          *10      *118 ネザーのみ
silverfish   1   *9    *7           *0       *64 高山のみ
```

メサの金鉱石は`net.minecraft.world.biome.BiomeMesa.Decorator.generateOres`にて、共用の`goldGen`を流用する形で生成している。そのため設定でサイズを変更すると影響を受ける（と思う）。高山の銀魚は`net.minecraft.world.biome.BiomeHills`を参照。

高山のエメラルドは`net.minecraft.world.biome.BiomeHills.EmeraldGenerator`であり、実は`WorldGenMinable`ではない。エメラルドの生成回数や座標などはすべて同クラスが支配している。エメラルドは特殊な生成ルーチンであるため、今回は扱わない。

### シルバーフィッシュスポーナーの謎

現バージョンのMinecraft Forgeの高山のシルバーフィッシュスポーナーの生成コードを見てみると、一つの謎がある。

```java:BiomeHills#decorate内
for (int j1 = 0; j1 < 7; ++j1) {
 int k1 = p_decorate_2_.nextInt(16);
 int l1 = p_decorate_2_.nextInt(64);
 int i2 = p_decorate_2_.nextInt(16);
 if (!TerrainGen.generateOre(world, random, silverfishSpawner, chunkPos.add(j1, k1, l1), OreGenEvent.GenerateMinable.EventType.SILVERFISH)) {
   continue;
 }
 silverfishSpawner.generate(world, random, chunkPos.add(k1, l1, i2));
}
```

生成基点座標`j1, k1, l1`は、`何回目の試行か, X座標加算値, Y座標加算値`となるので生成可否判定がおかしな座標で行われてしまう。実際に生成される場所は違和感のない記述である。

## 鉱石の追加方法

### 地上

MODで地上の鉱石を追加するには、`MinecraftForge.ORE_GEN_BUS`に対して`OreGenEvent.Pre`または`OreGenEvent.Post`をフックすればよい。`Miragium`という鉱石を追加するサンプルを載せる。これは`FMLPreInitializationEvent`の末尾にでも呼び出せばよい。

```java:
MinecraftForge.ORE_GEN_BUS.register(new Object() {

 private WorldGenMinable worldGenMiragium = new WorldGenMinable(blockOre.getState(BlockOre.EnumType.MIRAGIUM), 4);

 @SubscribeEvent
 public void accept(OreGenEvent.Post event)
 {
  genStandardOre(event, 16, worldGenMiragium, 64, 255);
 }

 private void genStandardOre(OreGenEvent.Post event, int count, WorldGenerator generator, int minHeightInclusive, int maxHeightExclusive)
 {
  for (int j = 0; j < count; ++j) {
   generator.generate(event.getWorld(), event.getRand(), event.getPos().add(
    event.getRand().nextInt(16),
    event.getRand().nextInt(maxHeightExclusive - minHeightInclusive) + minHeightInclusive,
    event.getRand().nextInt(16)));
  }
 }

});
```

### 地上以外

今のところ未調査。実は地上用のコードがネザーの地形生成にも侵食しているかもしれない（しかし置換元となる焼き石が存在しないので実害がない（気づけない））。