298 lines
12 KiB
Java
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|