package net.minecraft.client.multiplayer; import com.mojang.logging.LogUtils; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.Minecraft; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.Registries; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LightLayer; import net.minecraft.world.level.biome.Biomes; import net.minecraft.world.level.chunk.ChunkSource; import net.minecraft.world.level.chunk.EmptyLevelChunk; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.lighting.LevelLightEngine; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class ClientChunkCache extends ChunkSource { static final Logger LOGGER = LogUtils.getLogger(); private final LevelChunk emptyChunk; private final LevelLightEngine lightEngine; volatile ClientChunkCache.Storage storage; final ClientLevel level; public ClientChunkCache(ClientLevel level, int viewDistance) { this.level = level; this.emptyChunk = new EmptyLevelChunk(level, new ChunkPos(0, 0), level.registryAccess().lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS)); this.lightEngine = new LevelLightEngine(this, true, level.dimensionType().hasSkyLight()); this.storage = new ClientChunkCache.Storage(calculateStorageRange(viewDistance)); } @Override public LevelLightEngine getLightEngine() { return this.lightEngine; } private static boolean isValidChunk(@Nullable LevelChunk chunk, int x, int z) { if (chunk == null) { return false; } else { ChunkPos chunkPos = chunk.getPos(); return chunkPos.x == x && chunkPos.z == z; } } public void drop(ChunkPos chunkPos) { if (this.storage.inRange(chunkPos.x, chunkPos.z)) { int i = this.storage.getIndex(chunkPos.x, chunkPos.z); LevelChunk levelChunk = this.storage.getChunk(i); if (isValidChunk(levelChunk, chunkPos.x, chunkPos.z)) { this.storage.drop(i, levelChunk); } } } @Nullable public LevelChunk getChunk(int i, int j, ChunkStatus chunkStatus, boolean bl) { if (this.storage.inRange(i, j)) { LevelChunk levelChunk = this.storage.getChunk(this.storage.getIndex(i, j)); if (isValidChunk(levelChunk, i, j)) { return levelChunk; } } return bl ? this.emptyChunk : null; } @Override public BlockGetter getLevel() { return this.level; } public void replaceBiomes(int x, int z, FriendlyByteBuf buffer) { if (!this.storage.inRange(x, z)) { LOGGER.warn("Ignoring chunk since it's not in the view range: {}, {}", x, z); } else { int i = this.storage.getIndex(x, z); LevelChunk levelChunk = (LevelChunk)this.storage.chunks.get(i); if (!isValidChunk(levelChunk, x, z)) { LOGGER.warn("Ignoring chunk since it's not present: {}, {}", x, z); } else { levelChunk.replaceBiomes(buffer); } } } @Nullable public LevelChunk replaceWithPacketData( int x, int z, FriendlyByteBuf readBuffer, Map heightmaps, Consumer consumer ) { if (!this.storage.inRange(x, z)) { LOGGER.warn("Ignoring chunk since it's not in the view range: {}, {}", x, z); return null; } else { int i = this.storage.getIndex(x, z); LevelChunk levelChunk = (LevelChunk)this.storage.chunks.get(i); ChunkPos chunkPos = new ChunkPos(x, z); if (!isValidChunk(levelChunk, x, z)) { levelChunk = new LevelChunk(this.level, chunkPos); levelChunk.replaceWithPacketData(readBuffer, heightmaps, consumer); this.storage.replace(i, levelChunk); } else { levelChunk.replaceWithPacketData(readBuffer, heightmaps, consumer); this.storage.refreshEmptySections(levelChunk); } this.level.onChunkLoaded(chunkPos); return levelChunk; } } @Override public void tick(BooleanSupplier hasTimeLeft, boolean tickChunks) { } public void updateViewCenter(int x, int z) { this.storage.viewCenterX = x; this.storage.viewCenterZ = z; } public void updateViewRadius(int viewDistance) { int i = this.storage.chunkRadius; int j = calculateStorageRange(viewDistance); if (i != j) { ClientChunkCache.Storage storage = new ClientChunkCache.Storage(j); storage.viewCenterX = this.storage.viewCenterX; storage.viewCenterZ = this.storage.viewCenterZ; for (int k = 0; k < this.storage.chunks.length(); k++) { LevelChunk levelChunk = (LevelChunk)this.storage.chunks.get(k); if (levelChunk != null) { ChunkPos chunkPos = levelChunk.getPos(); if (storage.inRange(chunkPos.x, chunkPos.z)) { storage.replace(storage.getIndex(chunkPos.x, chunkPos.z), levelChunk); } } } this.storage = storage; } } private static int calculateStorageRange(int viewDistance) { return Math.max(2, viewDistance) + 3; } @Override public String gatherStats() { return this.storage.chunks.length() + ", " + this.getLoadedChunksCount(); } @Override public int getLoadedChunksCount() { return this.storage.chunkCount; } @Override public void onLightUpdate(LightLayer layer, SectionPos pos) { Minecraft.getInstance().levelRenderer.setSectionDirty(pos.x(), pos.y(), pos.z()); } public LongOpenHashSet getLoadedEmptySections() { return this.storage.loadedEmptySections; } @Override public void onSectionEmptinessChanged(int x, int y, int z, boolean isEmpty) { this.storage.onSectionEmptinessChanged(x, y, z, isEmpty); } @Environment(EnvType.CLIENT) final class Storage { final AtomicReferenceArray chunks; final LongOpenHashSet loadedEmptySections = new LongOpenHashSet(); final int chunkRadius; private final int viewRange; volatile int viewCenterX; volatile int viewCenterZ; int chunkCount; Storage(final int chunkRadius) { this.chunkRadius = chunkRadius; this.viewRange = chunkRadius * 2 + 1; this.chunks = new AtomicReferenceArray(this.viewRange * this.viewRange); } int getIndex(int x, int z) { return Math.floorMod(z, this.viewRange) * this.viewRange + Math.floorMod(x, this.viewRange); } void replace(int chunkIndex, @Nullable LevelChunk chunk) { LevelChunk levelChunk = (LevelChunk)this.chunks.getAndSet(chunkIndex, chunk); if (levelChunk != null) { this.chunkCount--; this.dropEmptySections(levelChunk); ClientChunkCache.this.level.unload(levelChunk); } if (chunk != null) { this.chunkCount++; this.addEmptySections(chunk); } } void drop(int chunkIndex, LevelChunk chunk) { if (this.chunks.compareAndSet(chunkIndex, chunk, null)) { this.chunkCount--; this.dropEmptySections(chunk); } ClientChunkCache.this.level.unload(chunk); } public void onSectionEmptinessChanged(int x, int y, int z, boolean isEmpty) { if (this.inRange(x, z)) { long l = SectionPos.asLong(x, y, z); if (isEmpty) { this.loadedEmptySections.add(l); } else if (this.loadedEmptySections.remove(l)) { ClientChunkCache.this.level.onSectionBecomingNonEmpty(l); } } } private void dropEmptySections(LevelChunk chunk) { LevelChunkSection[] levelChunkSections = chunk.getSections(); for (int i = 0; i < levelChunkSections.length; i++) { ChunkPos chunkPos = chunk.getPos(); this.loadedEmptySections.remove(SectionPos.asLong(chunkPos.x, chunk.getSectionYFromSectionIndex(i), chunkPos.z)); } } private void addEmptySections(LevelChunk chunk) { LevelChunkSection[] levelChunkSections = chunk.getSections(); for (int i = 0; i < levelChunkSections.length; i++) { LevelChunkSection levelChunkSection = levelChunkSections[i]; if (levelChunkSection.hasOnlyAir()) { ChunkPos chunkPos = chunk.getPos(); this.loadedEmptySections.add(SectionPos.asLong(chunkPos.x, chunk.getSectionYFromSectionIndex(i), chunkPos.z)); } } } void refreshEmptySections(LevelChunk chunk) { ChunkPos chunkPos = chunk.getPos(); LevelChunkSection[] levelChunkSections = chunk.getSections(); for (int i = 0; i < levelChunkSections.length; i++) { LevelChunkSection levelChunkSection = levelChunkSections[i]; long l = SectionPos.asLong(chunkPos.x, chunk.getSectionYFromSectionIndex(i), chunkPos.z); if (levelChunkSection.hasOnlyAir()) { this.loadedEmptySections.add(l); } else if (this.loadedEmptySections.remove(l)) { ClientChunkCache.this.level.onSectionBecomingNonEmpty(l); } } } boolean inRange(int x, int z) { return Math.abs(x - this.viewCenterX) <= this.chunkRadius && Math.abs(z - this.viewCenterZ) <= this.chunkRadius; } @Nullable protected LevelChunk getChunk(int chunkIndex) { return (LevelChunk)this.chunks.get(chunkIndex); } private void dumpChunks(String filePath) { try { FileOutputStream fileOutputStream = new FileOutputStream(filePath); try { int i = ClientChunkCache.this.storage.chunkRadius; for (int j = this.viewCenterZ - i; j <= this.viewCenterZ + i; j++) { for (int k = this.viewCenterX - i; k <= this.viewCenterX + i; k++) { LevelChunk levelChunk = (LevelChunk)ClientChunkCache.this.storage.chunks.get(ClientChunkCache.this.storage.getIndex(k, j)); if (levelChunk != null) { ChunkPos chunkPos = levelChunk.getPos(); fileOutputStream.write((chunkPos.x + "\t" + chunkPos.z + "\t" + levelChunk.isEmpty() + "\n").getBytes(StandardCharsets.UTF_8)); } } } } catch (Throwable var9) { try { fileOutputStream.close(); } catch (Throwable var8) { var9.addSuppressed(var8); } throw var9; } fileOutputStream.close(); } catch (IOException var10) { ClientChunkCache.LOGGER.error("Failed to dump chunks to file {}", filePath, var10); } } } }