異步線程獲取概述

本頁將概述 1.12 版本中異步線程獲取區塊的基礎代碼原理

漏洞起因 - 異步線程來源

BlockStainedGlass 類(染色玻璃) 中的 onBlockAddedbreakBlock (放置或破壞染色玻璃時),會呼叫 BlockBeacon.updateColorAsync 來更新烽火台光柱的顏色。

BlockStainedGlass.java
public void onBlockAdded(World worldIn, BlockPos pos, IBlockState state) {   if (!worldIn.isRemote) {
    BlockBeacon.updateColorAsync(worldIn, pos);   }
} 
public void breakBlock(World worldIn, BlockPos pos, IBlockState state) {   if (!worldIn.isRemote) {
    BlockBeacon.updateColorAsync(worldIn, pos);   }
} 

然而在 BlockBeacon.updateColorAsync 中創建了一個 新的執行緒 用來判斷,導致 worldIn.getChunk() 在非主執行緒讀取 區塊。

BlockBeacon.java
public static void updateColorAsync(final World worldIn, final BlockPos glassPos) {
  HttpUtil.DOWNLOADER_EXECUTOR.submit(new Runnable() {     public void run() {
      Chunk chunk = worldIn.getChunk(glassPos); 
      for (int i = glassPos.getY() - 1; i >= 0; --i) {
        final BlockPos blockpos = new BlockPos(glassPos.getX(), i, glassPos.getZ());

        if (!chunk.canSeeSky(blockpos)) break;

        IBlockState iblockstate = worldIn.getBlockState(blockpos);
        if (iblockstate.getBlock() == Blocks.BEACON) {
          ((WorldServer)worldIn).addScheduledTask(new Runnable() {
            public void run() {
              TileEntity tileentity = worldIn.getTileEntity(blockpos);

              if (tileentity instanceof TileEntityBeacon) {
                ((TileEntityBeacon)tileentity).updateBeacon();
                worldIn.addBlockEvent(blockpos, Blocks.BEACON, 1, 0);
              }
            }
          });
        }
      }
    }
  }); }

在正常情況下,getChunk 會嘗試 loadChunk 並獲取到對應的區塊,但在特殊情況下 loadChunk 會返回 null 從而觸發地形生成(生成基本地形,並裝飾)。

World.java
public Chunk getChunk(BlockPos pos) {
  return this.getChunk(pos.getX() >> 4, pos.getZ() >> 4); // 會呼叫到下面的那個函數}

public Chunk getChunk(int chunkX, int chunkZ) { // <- 呼叫這個  return this.chunkProvider.provideChunk(chunkX, chunkZ); }

public Chunk provideChunk(int x, int z) {   Chunk chunk = this.loadChunk(x, z);   if (chunk == null) {     long i = ChunkPos.asLong(x, z);
    try {
      chunk = this.chunkGenerator.generateChunk(x, z);     } catch (Throwable throwable) {
      CrashReport crashreport = CrashReport.makeCrashReport(throwable, "Exception generating new chunk");
      CrashReportCategory crashreportcategory = crashreport.makeCategory("Chunk to be generated");
      crashreportcategory.addCrashSection("Location", String.format("%d,%d", x, z));
      crashreportcategory.addCrashSection("Position hash", Long.valueOf(i));
      crashreportcategory.addCrashSection("Generator", this.chunkGenerator);
      throw new ReportedException(crashreport);
    }

    this.loadedChunks.put(i, chunk);
    chunk.onLoad();
    chunk.populate(this, this.chunkGenerator);   } 
  return chunk;
} 

其中我們需要的是裝飾處理 (chunk.populate(this, this.chunkGenerator)),在呼叫 chunk.populate 後會嘗試呼叫 generator.populate,在這裡會生成各式結構,同時也會生成水和岩漿等等。

Chunk.java
public void populate(IChunkProvider chunkProvider, IChunkGenerator chunkGenrator) {   Chunk chunk = chunkProvider.getLoadedChunk(this.x, this.z - 1);
  Chunk chunk1 = chunkProvider.getLoadedChunk(this.x + 1, this.z);
  Chunk chunk2 = chunkProvider.getLoadedChunk(this.x, this.z + 1);
  Chunk chunk3 = chunkProvider.getLoadedChunk(this.x - 1, this.z);

  if (chunk1 != null && chunk2 != null && chunkProvider.getLoadedChunk(this.x + 1, this.z + 1) != null) {
    this.populate(chunkGenrator); // 會呼叫到下面的那個函數 [裝飾自己]  }

  if (chunk3 != null && chunk2 != null && chunkProvider.getLoadedChunk(this.x - 1, this.z + 1) != null) {
    chunk3.populate(chunkGenrator); // 會呼叫到下面的那個函數 [裝飾旁邊]  }

  if (chunk != null && chunk1 != null && chunkProvider.getLoadedChunk(this.x + 1, this.z - 1) != null) {
    chunk.populate(chunkGenrator); // 會呼叫到下面的那個函數 [裝飾旁邊]  }

  if (chunk != null && chunk3 != null) {
    Chunk chunk4 = chunkProvider.getLoadedChunk(this.x - 1, this.z - 1);
    if (chunk4 != null) {
      chunk4.populate(chunkGenrator); // 會呼叫到下面的那個函數 [裝飾旁邊]    }
  }
} 
protected void populate(IChunkGenerator generator) { // <- 呼叫這個  if (this.isTerrainPopulated()) {
    if (generator.generateStructures(this, this.x, this.z)) {
      this.markDirty();
    }
  } else {
    this.checkLight();
    generator.populate(this.x, this.z);     this.markDirty();
  }
} 

在不同的緯度會有對應的不同處理辦法,下面的代碼是 ChunkGeneratorOverworld 裡的函數實現 (主世界)

ChunkGeneratorOverworld.java
public void populate(int x, int z) {   BlockFalling.fallInstantly = true;
  int i = x * 16;
  int j = z * 16;
  BlockPos blockpos = new BlockPos(i, 0, j);
  Biome biome = this.world.getBiome(blockpos.add(16, 0, 16));
  this.rand.setSeed(this.world.getSeed());
  long k = this.rand.nextLong() / 2L * 2L + 1L;
  long l = this.rand.nextLong() / 2L * 2L + 1L;
  this.rand.setSeed((long) x * k + (long) z * l ^ this.world.getSeed());
  boolean flag = false;
  ChunkPos chunkpos = new ChunkPos(x, z);

  if (this.mapFeaturesEnabled) {
    if (this.settings.useMineShafts) {
      this.mineshaftGenerator.generateStructure(this.world, this.rand, chunkpos);
    }

    // ... 其他結構生成
  }

  if (biome != Biomes.DESERT && biome != Biomes.DESERT_HILLS && this.settings.useWaterLakes && !flag
      && this.rand.nextInt(this.settings.waterLakeChance) == 0) {
    int i1 = this.rand.nextInt(16) + 8;
    int j1 = this.rand.nextInt(256);
    int k1 = this.rand.nextInt(16) + 8;
    (new WorldGenLakes(Blocks.WATER)).generate(this.world, this.rand, blockpos.add(i1, j1, k1)); // 生成水  }

  if (!flag && this.rand.nextInt(this.settings.lavaLakeChance / 10) == 0 && this.settings.useLavaLakes) {
    int i2 = this.rand.nextInt(16) + 8;
    int l2 = this.rand.nextInt(this.rand.nextInt(248) + 8);
    int k3 = this.rand.nextInt(16) + 8;

    if (l2 < this.world.getSeaLevel() || this.rand.nextInt(this.settings.lavaLakeChance / 8) == 0) {
      (new WorldGenLakes(Blocks.LAVA)).generate(this.world, this.rand, blockpos.add(i2, l2, k3)); // 生成岩漿    }
  }

  if (this.settings.useDungeons) {
    for (int j2 = 0; j2 < this.settings.dungeonChance; ++j2) {
      int i3 = this.rand.nextInt(16) + 8;
      int l3 = this.rand.nextInt(256);
      int l1 = this.rand.nextInt(16) + 8;
      (new WorldGenDungeons()).generate(this.world, this.rand, blockpos.add(i3, l3, l1));
    }
  }

  biome.decorate(this.world, this.rand, new BlockPos(i, 0, j));
  WorldEntitySpawner.performWorldGenSpawning(this.world, biome, i + 8, j + 8, 16, 16, this.rand);
  blockpos = blockpos.add(8, 0, 8);
  for (int k2 = 0; k2 < 16; ++k2) {
    for (int j3 = 0; j3 < 16; ++j3) {
      BlockPos blockpos1 = this.world.getPrecipitationHeight(blockpos.add(k2, 0, j3));
      BlockPos blockpos2 = blockpos1.down();

      if (this.world.canBlockFreezeWater(blockpos2)) {
        this.world.setBlockState(blockpos2, Blocks.ICE.getDefaultState(), 2);
      }

      if (this.world.canSnowAt(blockpos1, true)) {
        this.world.setBlockState(blockpos1, Blocks.SNOW_LAYER.getDefaultState(), 2);
      }
    }
  }

  BlockFalling.fallInstantly = false;
} 

在世界裝飾時的方塊變更和玩家或活塞變更方塊並無本質上的區別,因此若在這個新執行緒中執行放置方塊等操作,就會導致異步的方塊更新,也就是說若我們使用偵測器檢測到此異步的方塊更新,就會生成異步偵測器 (如何獲取將在下篇文章說明 [如果沒看到就是還在寫不然就是鴿了])。

簡單描述

在 Minecraft 1.12 中,染色玻璃在放置或破壞時,會透過 BlockBeacon.updateColorAsync非主執行緒 更新烽火台的顏色。

這個異步更新會呼叫 world.getChunk() 取得區塊,如果該區塊尚未生成,會觸發 區塊生成與裝飾(例如生成水、岩漿、地牢等結構)。

由於這些操作不是在主執行緒執行,會造成 異步方塊更新,也就是方塊狀態在非同步環境下改變,可能被偵測到並生成 異步偵測器

換句話說,放置或破壞染色玻璃,可能間接觸發世界生成與裝飾的非同步操作,這是異步線程獲取的核心問題。