minecraft-src/net/minecraft/client/multiplayer/ClientChunkCache.java
2025-07-04 03:45:38 +03:00

318 lines
10 KiB
Java

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<Heightmap.Types, long[]> heightmaps, Consumer<ClientboundLevelChunkPacketData.BlockEntityTagOutput> 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<LevelChunk> 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);
}
}
}
}