src: add ChunkUnloadingSystem

This commit is contained in:
luk
2026-01-28 20:46:13 +00:00
parent 2e3dfa4763
commit b842725530

View File

@@ -0,0 +1,167 @@
package com.hypixel.hytale.server.core.universe.world.storage.component;
import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.RemoveReason;
import com.hypixel.hytale.component.Resource;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.system.tick.RunWhenPausedSystem;
import com.hypixel.hytale.component.system.tick.TickingSystem;
import com.hypixel.hytale.math.shape.Box2D;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.server.core.modules.entity.player.ChunkTracker;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.chunk.ChunkFlag;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.server.core.universe.world.events.ecs.ChunkUnloadEvent;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.logging.Level;
public class ChunkUnloadingSystem extends TickingSystem<ChunkStore> implements RunWhenPausedSystem<ChunkStore> {
public static final double DESPERATE_UNLOAD_RAM_USAGE_THRESHOLD = 0.85;
public static final int DESPERATE_UNLOAD_MAX_POLL_COUNT = 3;
public static final int TICKS_BEFORE_CHUNK_UNLOADING_REMINDER = 5000;
public int ticksUntilUnloadingReminder = 5000;
public ChunkUnloadingSystem() {
}
@Override
public void tick(float dt, int systemIndex, @Nonnull Store<ChunkStore> store) {
ChunkUnloadingSystem.Data dataResource = store.getResource(ChunkStore.UNLOAD_RESOURCE);
World world = store.getExternalData().getWorld();
if (!world.getWorldConfig().canUnloadChunks()) {
this.ticksUntilUnloadingReminder--;
if (this.ticksUntilUnloadingReminder <= 0) {
world.getLogger().at(Level.INFO).log("This world has disabled chunk unloading");
this.ticksUntilUnloadingReminder = 5000;
}
} else {
int pollCount = 1;
double percentOfRAMUsed = 1.0 - (double) Runtime.getRuntime().freeMemory() / Runtime.getRuntime().maxMemory();
if (percentOfRAMUsed > 0.85) {
double desperatePercent = (percentOfRAMUsed - 0.85) / 0.15000000000000002;
pollCount = Math.max(MathUtil.ceil(desperatePercent * 3.0), 1);
}
dataResource.pollCount = pollCount;
if (dataResource.tick(dt)) {
dataResource.chunkTrackers.clear();
world.getEntityStore().getStore().forEachChunk(ChunkTracker.getComponentType(), ChunkUnloadingSystem::collectTrackers);
store.forEachEntityParallel(WorldChunk.getComponentType(), ChunkUnloadingSystem::tryUnload);
}
}
}
public static void tryUnload(int index, @Nonnull ArchetypeChunk<ChunkStore> archetypeChunk, @Nonnull CommandBuffer<ChunkStore> commandBuffer) {
Store<ChunkStore> store = commandBuffer.getStore();
World world = store.getExternalData().getWorld();
WorldChunk worldChunkComponent = archetypeChunk.getComponent(index, WorldChunk.getComponentType());
assert worldChunkComponent != null;
ChunkUnloadingSystem.Data dataResource = commandBuffer.getResource(ChunkStore.UNLOAD_RESOURCE);
ChunkTracker.ChunkVisibility chunkVisibility = getChunkVisibility(dataResource.chunkTrackers, worldChunkComponent.getIndex());
if (chunkVisibility == ChunkTracker.ChunkVisibility.HOT) {
worldChunkComponent.resetKeepAlive();
worldChunkComponent.resetActiveTimer();
} else {
Box2D keepLoaded = world.getWorldConfig().getChunkConfig().getKeepLoadedRegion();
boolean shouldKeepLoaded = worldChunkComponent.shouldKeepLoaded()
|| keepLoaded != null && isChunkInBox(keepLoaded, worldChunkComponent.getX(), worldChunkComponent.getZ());
int pollCount = dataResource.pollCount;
if (chunkVisibility == ChunkTracker.ChunkVisibility.COLD || worldChunkComponent.getNeedsSaving() || shouldKeepLoaded) {
worldChunkComponent.resetKeepAlive();
if (worldChunkComponent.is(ChunkFlag.TICKING) && worldChunkComponent.pollActiveTimer(pollCount) <= 0) {
commandBuffer.run(s -> worldChunkComponent.setFlag(ChunkFlag.TICKING, false));
}
} else if (worldChunkComponent.pollKeepAlive(pollCount) <= 0) {
Ref<ChunkStore> chunkRef = archetypeChunk.getReferenceTo(index);
ChunkUnloadEvent event = new ChunkUnloadEvent(worldChunkComponent);
commandBuffer.invoke(chunkRef, event);
if (event.isCancelled()) {
if (event.willResetKeepAlive()) {
worldChunkComponent.resetKeepAlive();
}
} else {
commandBuffer.run(s -> s.getExternalData().remove(chunkRef, RemoveReason.UNLOAD));
}
}
}
}
public static ChunkTracker.ChunkVisibility getChunkVisibility(@Nonnull List<ChunkTracker> playerChunkTrackers, long chunkIndex) {
boolean isVisible = false;
for (ChunkTracker chunkTracker : playerChunkTrackers) {
switch (chunkTracker.getChunkVisibility(chunkIndex)) {
case NONE:
default:
break;
case HOT:
return ChunkTracker.ChunkVisibility.HOT;
case COLD:
isVisible = true;
}
}
return isVisible ? ChunkTracker.ChunkVisibility.COLD : ChunkTracker.ChunkVisibility.NONE;
}
private static boolean isChunkInBox(@Nonnull Box2D box, int x, int z) {
int minX = ChunkUtil.minBlock(x);
int minZ = ChunkUtil.minBlock(z);
int maxX = ChunkUtil.maxBlock(x);
int maxZ = ChunkUtil.maxBlock(z);
return maxX >= box.min.x && minX <= box.max.x && maxZ >= box.min.y && minZ <= box.max.y;
}
private static void collectTrackers(@Nonnull ArchetypeChunk<EntityStore> archetypeChunk, @Nonnull CommandBuffer<EntityStore> commandBuffer) {
Store<ChunkStore> chunkStore = commandBuffer.getExternalData().getWorld().getChunkStore().getStore();
ChunkUnloadingSystem.Data dataResource = chunkStore.getResource(ChunkStore.UNLOAD_RESOURCE);
for (int index = 0; index < archetypeChunk.size(); index++) {
ChunkTracker chunkTracker = archetypeChunk.getComponent(index, ChunkTracker.getComponentType());
dataResource.chunkTrackers.add(chunkTracker);
}
}
public static class Data implements Resource<ChunkStore> {
public static final float UNLOAD_INTERVAL = 0.5F;
private float time;
private int pollCount = 1;
@Nonnull
private final List<ChunkTracker> chunkTrackers = new ObjectArrayList<>();
public Data() {
this.time = 0.5F;
}
public Data(float time) {
this.time = time;
}
@Nonnull
@Override
public Resource<ChunkStore> clone() {
return new ChunkUnloadingSystem.Data(this.time);
}
public boolean tick(float dt) {
this.time -= dt;
if (this.time <= 0.0F) {
this.time += 0.5F;
return true;
} else {
return false;
}
}
}
}