2025-11-24 22:52:51 +03:00

298 lines
12 KiB
Java

/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* com.mojang.logging.LogUtils
* it.unimi.dsi.fastutil.longs.LongOpenHashSet
* org.jspecify.annotations.Nullable
* org.slf4j.Logger
*/
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.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
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.jspecify.annotations.Nullable;
import org.slf4j.Logger;
public class ClientChunkCache
extends ChunkSource {
private static final Logger LOGGER = LogUtils.getLogger();
private final LevelChunk emptyChunk;
private final LevelLightEngine lightEngine;
private volatile Storage storage;
private final ClientLevel level;
public ClientChunkCache(ClientLevel level, int serverChunkRadius) {
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 Storage(ClientChunkCache.calculateStorageRange(serverChunkRadius));
}
@Override
public LevelLightEngine getLightEngine() {
return this.lightEngine;
}
private static boolean isValidChunk(@Nullable LevelChunk chunk, int x, int z) {
if (chunk == null) {
return false;
}
ChunkPos pos = chunk.getPos();
return pos.x == x && pos.z == z;
}
public void drop(ChunkPos pos) {
if (!this.storage.inRange(pos.x, pos.z)) {
return;
}
int index = this.storage.getIndex(pos.x, pos.z);
LevelChunk currentChunk = this.storage.getChunk(index);
if (ClientChunkCache.isValidChunk(currentChunk, pos.x, pos.z)) {
this.storage.drop(index, currentChunk);
}
}
@Override
public @Nullable LevelChunk getChunk(int x, int z, ChunkStatus targetStatus, boolean loadOrGenerate) {
LevelChunk chunk;
if (this.storage.inRange(x, z) && ClientChunkCache.isValidChunk(chunk = this.storage.getChunk(this.storage.getIndex(x, z)), x, z)) {
return chunk;
}
if (loadOrGenerate) {
return this.emptyChunk;
}
return null;
}
@Override
public BlockGetter getLevel() {
return this.level;
}
public void replaceBiomes(int chunkX, int chunkZ, FriendlyByteBuf readBuffer) {
if (!this.storage.inRange(chunkX, chunkZ)) {
LOGGER.warn("Ignoring chunk since it's not in the view range: {}, {}", (Object)chunkX, (Object)chunkZ);
return;
}
int index = this.storage.getIndex(chunkX, chunkZ);
LevelChunk chunk = this.storage.chunks.get(index);
if (!ClientChunkCache.isValidChunk(chunk, chunkX, chunkZ)) {
LOGGER.warn("Ignoring chunk since it's not present: {}, {}", (Object)chunkX, (Object)chunkZ);
} else {
chunk.replaceBiomes(readBuffer);
}
}
public @Nullable LevelChunk replaceWithPacketData(int chunkX, int chunkZ, FriendlyByteBuf readBuffer, Map<Heightmap.Types, long[]> heightmaps, Consumer<ClientboundLevelChunkPacketData.BlockEntityTagOutput> blockEntities) {
if (!this.storage.inRange(chunkX, chunkZ)) {
LOGGER.warn("Ignoring chunk since it's not in the view range: {}, {}", (Object)chunkX, (Object)chunkZ);
return null;
}
int index = this.storage.getIndex(chunkX, chunkZ);
LevelChunk chunk = this.storage.chunks.get(index);
ChunkPos pos = new ChunkPos(chunkX, chunkZ);
if (!ClientChunkCache.isValidChunk(chunk, chunkX, chunkZ)) {
chunk = new LevelChunk(this.level, pos);
chunk.replaceWithPacketData(readBuffer, heightmaps, blockEntities);
this.storage.replace(index, chunk);
} else {
chunk.replaceWithPacketData(readBuffer, heightmaps, blockEntities);
this.storage.refreshEmptySections(chunk);
}
this.level.onChunkLoaded(pos);
return chunk;
}
@Override
public void tick(BooleanSupplier haveTime, boolean tickChunks) {
}
public void updateViewCenter(int x, int z) {
this.storage.viewCenterX = x;
this.storage.viewCenterZ = z;
}
public void updateViewRadius(int viewRange) {
int chunkRadius = this.storage.chunkRadius;
int newChunkRadius = ClientChunkCache.calculateStorageRange(viewRange);
if (chunkRadius != newChunkRadius) {
Storage newStorage = new Storage(newChunkRadius);
newStorage.viewCenterX = this.storage.viewCenterX;
newStorage.viewCenterZ = this.storage.viewCenterZ;
for (int i = 0; i < this.storage.chunks.length(); ++i) {
LevelChunk chunk = this.storage.chunks.get(i);
if (chunk == null) continue;
ChunkPos pos = chunk.getPos();
if (!newStorage.inRange(pos.x, pos.z)) continue;
newStorage.replace(newStorage.getIndex(pos.x, pos.z), chunk);
}
this.storage = newStorage;
}
}
private static int calculateStorageRange(int viewRange) {
return Math.max(2, viewRange) + 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 sectionX, int sectionY, int sectionZ, boolean empty) {
this.storage.onSectionEmptinessChanged(sectionX, sectionY, sectionZ, empty);
}
private final class Storage {
private final AtomicReferenceArray<@Nullable LevelChunk> chunks;
private final LongOpenHashSet loadedEmptySections = new LongOpenHashSet();
private final int chunkRadius;
private final int viewRange;
private volatile int viewCenterX;
private volatile int viewCenterZ;
private int chunkCount;
private Storage(int chunkRadius) {
this.chunkRadius = chunkRadius;
this.viewRange = chunkRadius * 2 + 1;
this.chunks = new AtomicReferenceArray(this.viewRange * this.viewRange);
}
private int getIndex(int chunkX, int chunkZ) {
return Math.floorMod(chunkZ, this.viewRange) * this.viewRange + Math.floorMod(chunkX, this.viewRange);
}
private void replace(int index, @Nullable LevelChunk newChunk) {
LevelChunk removedChunk = this.chunks.getAndSet(index, newChunk);
if (removedChunk != null) {
--this.chunkCount;
this.dropEmptySections(removedChunk);
ClientChunkCache.this.level.unload(removedChunk);
}
if (newChunk != null) {
++this.chunkCount;
this.addEmptySections(newChunk);
}
}
private void drop(int index, LevelChunk oldChunk) {
if (this.chunks.compareAndSet(index, oldChunk, null)) {
--this.chunkCount;
this.dropEmptySections(oldChunk);
}
ClientChunkCache.this.level.unload(oldChunk);
}
public void onSectionEmptinessChanged(int sectionX, int sectionY, int sectionZ, boolean empty) {
if (!this.inRange(sectionX, sectionZ)) {
return;
}
long sectionNode = SectionPos.asLong(sectionX, sectionY, sectionZ);
if (empty) {
this.loadedEmptySections.add(sectionNode);
} else if (this.loadedEmptySections.remove(sectionNode)) {
ClientChunkCache.this.level.onSectionBecomingNonEmpty(sectionNode);
}
}
private void dropEmptySections(LevelChunk chunk) {
LevelChunkSection[] sections = chunk.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; ++sectionIndex) {
ChunkPos chunkPos = chunk.getPos();
this.loadedEmptySections.remove(SectionPos.asLong(chunkPos.x, chunk.getSectionYFromSectionIndex(sectionIndex), chunkPos.z));
}
}
private void addEmptySections(LevelChunk chunk) {
LevelChunkSection[] sections = chunk.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; ++sectionIndex) {
LevelChunkSection section = sections[sectionIndex];
if (!section.hasOnlyAir()) continue;
ChunkPos chunkPos = chunk.getPos();
this.loadedEmptySections.add(SectionPos.asLong(chunkPos.x, chunk.getSectionYFromSectionIndex(sectionIndex), chunkPos.z));
}
}
private void refreshEmptySections(LevelChunk chunk) {
ChunkPos chunkPos = chunk.getPos();
LevelChunkSection[] sections = chunk.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; ++sectionIndex) {
LevelChunkSection section = sections[sectionIndex];
long sectionNode = SectionPos.asLong(chunkPos.x, chunk.getSectionYFromSectionIndex(sectionIndex), chunkPos.z);
if (section.hasOnlyAir()) {
this.loadedEmptySections.add(sectionNode);
continue;
}
if (!this.loadedEmptySections.remove(sectionNode)) continue;
ClientChunkCache.this.level.onSectionBecomingNonEmpty(sectionNode);
}
}
private boolean inRange(int chunkX, int chunkZ) {
return Math.abs(chunkX - this.viewCenterX) <= this.chunkRadius && Math.abs(chunkZ - this.viewCenterZ) <= this.chunkRadius;
}
protected @Nullable LevelChunk getChunk(int index) {
return this.chunks.get(index);
}
private void dumpChunks(String file) {
try (FileOutputStream stream = new FileOutputStream(file);){
int chunkRadius = ClientChunkCache.this.storage.chunkRadius;
for (int z = this.viewCenterZ - chunkRadius; z <= this.viewCenterZ + chunkRadius; ++z) {
for (int x = this.viewCenterX - chunkRadius; x <= this.viewCenterX + chunkRadius; ++x) {
LevelChunk chunk = ClientChunkCache.this.storage.chunks.get(ClientChunkCache.this.storage.getIndex(x, z));
if (chunk == null) continue;
ChunkPos pos = chunk.getPos();
stream.write((pos.x + "\t" + pos.z + "\t" + chunk.isEmpty() + "\n").getBytes(StandardCharsets.UTF_8));
}
}
}
catch (IOException e) {
LOGGER.error("Failed to dump chunks to file {}", (Object)file, (Object)e);
}
}
}
}