diff --git a/src/com/hypixel/hytale/server/core/universe/world/storage/component/ChunkUnloadingSystem.java b/src/com/hypixel/hytale/server/core/universe/world/storage/component/ChunkUnloadingSystem.java new file mode 100644 index 00000000..248d1351 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/universe/world/storage/component/ChunkUnloadingSystem.java @@ -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 implements RunWhenPausedSystem { + 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 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 archetypeChunk, @Nonnull CommandBuffer commandBuffer) { + Store 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 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 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 archetypeChunk, @Nonnull CommandBuffer commandBuffer) { + Store 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 { + public static final float UNLOAD_INTERVAL = 0.5F; + private float time; + private int pollCount = 1; + @Nonnull + private final List chunkTrackers = new ObjectArrayList<>(); + + public Data() { + this.time = 0.5F; + } + + public Data(float time) { + this.time = time; + } + + @Nonnull + @Override + public Resource 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; + } + } + } +}