From 036b42d19704fe77a2ecd67da721fbf8fdc7ae28 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Tue, 7 Jan 2025 19:18:21 -0600 Subject: [PATCH] Add world gen progress updates to the overlay --- ...stantGeneratorProgressDisplayLocation.java | 44 ++++++ .../core/api/internal/ClientApi.java | 20 ++- .../distanthorizons/core/config/Config.java | 25 ++- .../FullDataSourceProviderV2.java | 10 +- .../GeneratedFullDataSourceProvider.java | 20 +-- .../IFullDataSourceRetrievalQueue.java | 17 +- .../generation/RemoteWorldRetrievalQueue.java | 27 +++- .../core/generation/WorldGenerationQueue.java | 50 +++++- .../core/level/WorldGenModule.java | 145 +++++++++++++++++- .../core/logging/f3/F3Screen.java | 2 +- .../core/pos/DhSectionPos.java | 4 +- .../core/render/LodQuadTree.java | 23 ++- .../core/render/LodRenderSection.java | 17 ++ .../minecraft/IMinecraftClientWrapper.java | 2 + .../assets/distanthorizons/lang/en_us.json | 23 ++- 15 files changed, 371 insertions(+), 58 deletions(-) create mode 100644 api/src/main/java/com/seibel/distanthorizons/api/enums/worldGeneration/EDhApiDistantGeneratorProgressDisplayLocation.java diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/worldGeneration/EDhApiDistantGeneratorProgressDisplayLocation.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/worldGeneration/EDhApiDistantGeneratorProgressDisplayLocation.java new file mode 100644 index 000000000..698927add --- /dev/null +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/worldGeneration/EDhApiDistantGeneratorProgressDisplayLocation.java @@ -0,0 +1,44 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.api.enums.worldGeneration; + +/** + * OVERLAY
+ * CHAT
+ * LOG
+ * DISABLED

+ * + * @author James Seibel + * @version 2025-1-7 + * @since API 4.0.0 + */ +public enum EDhApiDistantGeneratorProgressDisplayLocation +{ + // Reminder: + // when adding items up the API minor version + // when removing items up the API major version + + + OVERLAY, + CHAT, + LOG, + DISABLED; + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java index c8e779f34..32db1efcc 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java @@ -99,6 +99,7 @@ public class ClientApi private long lastStaticWarningMessageSentMsTime = 0L; private final Queue chatMessageQueueForNextFrame = new LinkedBlockingQueue<>(); + private final Queue overlayMessageQueueForNextFrame = new LinkedBlockingQueue<>(); public boolean rendererDisabledBecauseOfExceptions = false; @@ -624,7 +625,7 @@ public class ClientApi } - // generic messages + // chat messages while (!this.chatMessageQueueForNextFrame.isEmpty()) { String message = this.chatMessageQueueForNextFrame.poll(); @@ -635,6 +636,18 @@ public class ClientApi } MC_CLIENT.sendChatMessage(message); } + + // overlay messages + while (!this.overlayMessageQueueForNextFrame.isEmpty()) + { + String message = this.overlayMessageQueueForNextFrame.poll(); + if (message == null) + { + // done to prevent potential null pointers + message = ""; + } + MC_CLIENT.sendOverlayMessage(message); + } } private void detectAndSendBootTimeWarnings() { @@ -722,4 +735,9 @@ public class ClientApi */ public void showChatMessageNextFrame(String chatMessage) { this.chatMessageQueueForNextFrame.add(chatMessage); } + /** + * Similar to {@link ClientApi#showChatMessageNextFrame(String)} but appears above the toolbar. + */ + public void showOverlayMessageNextFrame(String message) { this.overlayMessageQueueForNextFrame.add(message); } + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java index 3d80d0800..4990778d3 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java @@ -24,6 +24,7 @@ import com.seibel.distanthorizons.api.enums.config.*; import com.seibel.distanthorizons.api.enums.config.quickOptions.*; import com.seibel.distanthorizons.api.enums.rendering.*; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode; +import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorProgressDisplayLocation; import com.seibel.distanthorizons.core.config.eventHandlers.*; import com.seibel.distanthorizons.core.config.eventHandlers.presets.*; import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; @@ -1193,7 +1194,10 @@ public class Config .set(true) .comment("" + " Should Distant Horizons slowly generate LODs \n" - + " outside the vanilla render distance?") + + " outside the vanilla render distance? \n" + + "Depending on the generator mode, this will import existing chunks \n" + + "and/or generating missing chunks." + + "") .build(); public static ConfigEntry distantGeneratorMode = new ConfigEntry.Builder() @@ -1237,6 +1241,25 @@ public class Config + "") .build(); + public static ConfigEntry showGenerationProgress = new ConfigEntry.Builder() + .set(EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY) + .comment("" + + "How should distant generator progress be displayed? \n" + + "\n" + + EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY + ": may be the same as "+EDhApiDistantGeneratorProgressDisplayLocation.CHAT+" for some Minecraft versions \n" + + EDhApiDistantGeneratorProgressDisplayLocation.CHAT + " \n" + + EDhApiDistantGeneratorProgressDisplayLocation.LOG + " \n" + + EDhApiDistantGeneratorProgressDisplayLocation.DISABLED + " \n" + + "") + .build(); + + public static ConfigEntry generationProgressDisplayIntervalInSeconds = new ConfigEntry.Builder() + .setMinDefaultMax(1, 2, 60 * 60 * 4) // max = 4 hours + .comment("" + + "How often should the distant generator progress be displayed? \n" + + "") + .build(); + } public static class LodBuilding diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java index a7d5200de..6ae54b04c 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/FullDataSourceProviderV2.java @@ -40,7 +40,6 @@ import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV2Repo; import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; -import com.seibel.distanthorizons.core.util.threading.PrioritySemaphore; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import it.unimi.dsi.fastutil.longs.LongArrayList; @@ -611,12 +610,7 @@ public class FullDataSourceProviderV2 */ @Nullable public LongArrayList getPositionsToRetrieve(Long pos) { return null; } - /** - * Returns how many positions could potentially be generated for this position assuming the position is empty. - * Used when estimating the total number of retrieval requests. - */ - public int getMaxPossibleRetrievalPositionCountForPos(Long pos) { return -1; } - + /** @return true if the position was queued, false if not */ @Nullable public CompletableFuture queuePositionForRetrieval(Long genPos) { return null; } @@ -628,6 +622,8 @@ public class FullDataSourceProviderV2 /** Can be used to display how many total retrieval requests might be available. */ public void setTotalRetrievalPositionCount(int newCount) { } + /** Can be used to display how many total chunk retrieval requests should be available. */ + public void setEstimatedRemainingRetrievalChunkCount(int newCount) { } /** * Returns how many data sources are currently in memory and haven't diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java index 91f74da6b..2edd1d6d5 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/GeneratedFullDataSourceProvider.java @@ -37,7 +37,6 @@ import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; -import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList; import org.apache.logging.log4j.Logger; @@ -168,12 +167,12 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im public boolean canRetrieveMissingDataSources() { return true; } @Override - public void setTotalRetrievalPositionCount(int newCount) + public void setEstimatedRemainingRetrievalChunkCount(int newCount) { IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); if (worldGenQueue != null) { - worldGenQueue.setEstimatedTotalTaskCount(newCount); + worldGenQueue.setRetrievalEstimatedRemainingChunkCount(newCount); } } @@ -411,21 +410,6 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im return generationList; } - @Override - public int getMaxPossibleRetrievalPositionCountForPos(Long pos) - { - IFullDataSourceRetrievalQueue worldGenQueue = this.worldGenQueueRef.get(); - if (worldGenQueue == null) - { - return -1; - } - - int minGeneratorSectionDetailLevel = worldGenQueue.highestDataDetail() + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; - int detailLevelDiff = DhSectionPos.getDetailLevel(pos) - minGeneratorSectionDetailLevel; - - return BitShiftUtil.powerOfTwo(detailLevelDiff); - } - //=======// diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/IFullDataSourceRetrievalQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/IFullDataSourceRetrievalQueue.java index 05a103e83..a81f871f7 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/IFullDataSourceRetrievalQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/IFullDataSourceRetrievalQueue.java @@ -24,6 +24,8 @@ import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.render.LodQuadTree; +import com.seibel.distanthorizons.core.util.objects.RollingAverage; +import org.jetbrains.annotations.Nullable; import java.io.Closeable; import java.util.List; @@ -113,11 +115,22 @@ public interface IFullDataSourceRetrievalQueue extends Closeable int getWaitingTaskCount(); int getInProgressTaskCount(); + /** returns how many chunks are currently queued for retrieval */ + int getQueuedChunkCount(); + /** used for rendering to the F3 menu */ - int getEstimatedTotalTaskCount(); - void setEstimatedTotalTaskCount(int newEstimate); + int getEstimatedRemainingTaskCount(); + void setEstimatedRemainingTaskCount(int newEstimate); + + /** used for displaying a progress update to the user */ + int getRetrievalEstimatedRemainingChunkCount(); + void setRetrievalEstimatedRemainingChunkCount(int newEstimate); void addDebugMenuStringsToList(List messageList); + /** Can be used to determine roughly how fast the world generator is running. */ + RollingAverage getRollingAverageChunkGenTimeInMs(); + + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/RemoteWorldRetrievalQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/RemoteWorldRetrievalQueue.java index bef67c9a6..4d30db140 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/RemoteWorldRetrievalQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/RemoteWorldRetrievalQueue.java @@ -11,6 +11,7 @@ import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.objects.RollingAverage; import org.apache.logging.log4j.Logger; import java.util.ArrayList; @@ -21,7 +22,11 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - private int estimatedTotalTaskCount; + private int estimatedRemainingTaskCount; + private int estimatedTotalChunkCount; + + private final RollingAverage rollingAverageChunkGenTimeInMs = new RollingAverage(1_000); + public RollingAverage getRollingAverageChunkGenTimeInMs() { return this.rollingAverageChunkGenTimeInMs; } @@ -49,9 +54,18 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue @Override public CompletableFuture submitRetrievalTask(long sectionPos, byte requiredDataDetail, IWorldGenTaskTracker tracker) { + long generationStartMsTime = System.currentTimeMillis(); + return super.submitRequest(sectionPos, tracker.getDataSourceConsumer()) .thenApply(requestResult -> { + long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime; + + int chunkWidth = DhSectionPos.getChunkWidth(sectionPos); + int chunkCount = chunkWidth * chunkWidth; + double timePerChunk = (double)totalGenTimeInMs / (double)chunkCount; + this.rollingAverageChunkGenTimeInMs.addValue(timePerChunk); + switch (requestResult) { case SUCCEEDED: @@ -94,10 +108,17 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue //===============// @Override - public int getEstimatedTotalTaskCount() { return this.estimatedTotalTaskCount; } + public int getEstimatedRemainingTaskCount() { return this.estimatedRemainingTaskCount; } @Override - public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; } + public void setEstimatedRemainingTaskCount(int newEstimate) { this.estimatedRemainingTaskCount = newEstimate; } + @Override + public int getRetrievalEstimatedRemainingChunkCount() { return this.estimatedTotalChunkCount; } + @Override + public void setRetrievalEstimatedRemainingChunkCount(int newEstimate) { this.estimatedTotalChunkCount = newEstimate; } + + @Override + public int getQueuedChunkCount() { return 0; } } \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java index e236674c8..f52f476c4 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java @@ -42,6 +42,7 @@ import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.util.LodUtil.AssertFailureException; import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; +import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.objects.UncheckedInterruptedException; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; @@ -98,7 +99,11 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb private DhBlockPos2D generationTargetPos = DhBlockPos2D.ZERO; /** just used for rendering to the F3 menu */ - private int estimatedTotalTaskCount = 0; + private int estimatedRemainingTaskCount = 0; + private int estimatedRemainingChunkCount = 0; + + private final RollingAverage rollingAverageChunkGenTimeInMs = new RollingAverage(1_000); + public RollingAverage getRollingAverageChunkGenTimeInMs() { return this.rollingAverageChunkGenTimeInMs; } @@ -247,7 +252,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb */ private boolean startNextWorldGenTask(DhBlockPos2D targetPos) { - if (this.waitingTasks.size() == 0) + if (this.waitingTasks.isEmpty()) { return false; } @@ -333,7 +338,19 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb long taskPos = newTaskGroup.group.pos; LodUtil.assertTrue(taskDetailLevel >= this.highestDataDetail && taskDetailLevel <= this.lowestDataDetail); - newTaskGroup.genFuture = this.startGenerationEvent(taskPos, taskDetailLevel, newTaskGroup.group::consumeDataSource); + int generationRequestChunkWidthCount = BitShiftUtil.powerOfTwo(DhSectionPos.getDetailLevel(taskPos) - taskDetailLevel - 4); // minus 4 is equal to dividing by 16 to convert to chunk scale + + long generationStartMsTime = System.currentTimeMillis(); + CompletableFuture generationFuture = this.startGenerationEvent(taskPos, taskDetailLevel, generationRequestChunkWidthCount, newTaskGroup.group::consumeDataSource); + generationFuture.thenRun(() -> + { + long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime; + int chunkCount = generationRequestChunkWidthCount * generationRequestChunkWidthCount; + double timePerChunk = (double)totalGenTimeInMs / (double)chunkCount; + this.rollingAverageChunkGenTimeInMs.addValue(timePerChunk); + }); + + newTaskGroup.genFuture = generationFuture; LodUtil.assertTrue(newTaskGroup.genFuture != null); newTaskGroup.genFuture.whenComplete((voidObj, exception) -> @@ -369,11 +386,11 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb private CompletableFuture startGenerationEvent( long requestPos, byte targetDataDetail, + int generationRequestChunkWidthCount, Consumer dataSourceConsumer ) { DhChunkPos chunkPosMin = new DhChunkPos(DhSectionPos.getSectionBBoxPos(requestPos).getCornerBlockPos()); - int generationRequestChunkWidthCount = BitShiftUtil.powerOfTwo(DhSectionPos.getDetailLevel(requestPos) - targetDataDetail - 4); // minus 4 is equal to dividing by 16 to convert to chunk scale EDhApiDistantGeneratorMode generatorMode = Config.Common.WorldGenerator.distantGeneratorMode.get(); EDhApiWorldGeneratorReturnType returnType = this.generator.getReturnType(); @@ -495,11 +512,30 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb public byte highestDataDetail() { return this.highestDataDetail; } @Override - public int getEstimatedTotalTaskCount() { return this.estimatedTotalTaskCount; } + public int getEstimatedRemainingTaskCount() { return this.estimatedRemainingTaskCount; } @Override - public void setEstimatedTotalTaskCount(int newEstimate) { this.estimatedTotalTaskCount = newEstimate; } + public void setEstimatedRemainingTaskCount(int newEstimate) { this.estimatedRemainingTaskCount = newEstimate; } - @Override public void addDebugMenuStringsToList(List messageList) { } + @Override + public int getRetrievalEstimatedRemainingChunkCount() { return this.estimatedRemainingChunkCount; } + @Override + public void setRetrievalEstimatedRemainingChunkCount(int newEstimate) { this.estimatedRemainingChunkCount = newEstimate; } + + @Override + public void addDebugMenuStringsToList(List messageList) { } + + @Override + public int getQueuedChunkCount() + { + int chunkCount = 0; + for (long pos : this.waitingTasks.keySet()) + { + int chunkWidth = DhSectionPos.getBlockWidth(pos) / LodUtil.CHUNK_WIDTH; + chunkCount += (chunkWidth * chunkWidth); + } + + return chunkCount; + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java b/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java index e99d56d2c..ba7da3c55 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/WorldGenModule.java @@ -19,21 +19,25 @@ package com.seibel.distanthorizons.core.level; +import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorProgressDisplayLocation; +import com.seibel.distanthorizons.core.api.internal.ClientApi; +import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider; import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; +import com.seibel.distanthorizons.core.util.ThreadUtil; +import com.seibel.distanthorizons.core.util.objects.RollingAverage; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.world.DhApiWorldProxy; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.Nullable; import java.io.Closeable; import java.util.List; -import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BooleanSupplier; import java.util.function.Supplier; /** @@ -188,10 +192,20 @@ public class WorldGenModule implements Closeable } + // estimated tasks String waitingCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getWaitingTaskCount()); String inProgressCountStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getInProgressTaskCount()); - String totalCountEstimateStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getEstimatedTotalTaskCount()); - messageList.add("World Gen/Pull Chunk Tasks: "+waitingCountStr+"/"+totalCountEstimateStr+" (in progress "+inProgressCountStr+")"); + String totalCountEstimateStr = F3Screen.NUMBER_FORMAT.format(worldGenState.worldGenerationQueue.getRetrievalEstimatedRemainingChunkCount()); + String message = "World Gen/Import Tasks: "+waitingCountStr+"/"+totalCountEstimateStr+" (in progress "+inProgressCountStr+")"; + + // estimated chunks/sec + double chunksPerSec = worldGenState.getEstimatedChunksPerSecond(); + if (chunksPerSec > -1) + { + message += ", " + F3Screen.NUMBER_FORMAT.format(chunksPerSec) + " chunks/sec"; + } + + messageList.add(message); worldGenState.worldGenerationQueue.addDebugMenuStringsToList(messageList); } @@ -207,8 +221,15 @@ public class WorldGenModule implements Closeable { public IFullDataSourceRetrievalQueue worldGenerationQueue; + private static final ThreadPoolExecutor PROGRESS_UPDATER_THREAD = ThreadUtil.makeSingleThreadPool("World Gen Progress Updater"); + private boolean progressUpdateThreadRunning = false; + + CompletableFuture closeAsync(boolean doInterrupt) { + // this should stop the updater thread + this.progressUpdateThreadRunning = false; + return this.worldGenerationQueue.startClosingAsync(true, doInterrupt) .exceptionally(e -> { @@ -225,7 +246,119 @@ public class WorldGenModule implements Closeable /** @param targetPosForGeneration the position that world generation should be centered around */ public void startGenerationQueueAndSetTargetPos(DhBlockPos2D targetPosForGeneration) - { this.worldGenerationQueue.startAndSetTargetPos(targetPosForGeneration); } + { + this.worldGenerationQueue.startAndSetTargetPos(targetPosForGeneration); + this.startProgressUpdateThread(); + } + private void startProgressUpdateThread() + { + // only start the thread once + if (!this.progressUpdateThreadRunning) + { + this.progressUpdateThreadRunning = true; + + PROGRESS_UPDATER_THREAD.execute(() -> + { + while (this.progressUpdateThreadRunning) + { + try + { + this.sendRetrievalProgress(); + + // sleep so we only see an update once in a while + int sleepTimeInSec = Config.Common.WorldGenerator.generationProgressDisplayIntervalInSeconds.get(); + Thread.sleep(sleepTimeInSec * 1_000L); + } + catch (Exception e) + { + LOGGER.error("Unexpected issue displaying chunk retrieval progress [" + e.getMessage() + "].", e); + } + } + }); + } + } + private void sendRetrievalProgress() + { + int remainingChunkCount = this.worldGenerationQueue.getRetrievalEstimatedRemainingChunkCount(); + remainingChunkCount += this.worldGenerationQueue.getQueuedChunkCount(); + String remainingChunkCountStr = F3Screen.NUMBER_FORMAT.format(remainingChunkCount); + + String message = "DH Gen/Import: " + remainingChunkCountStr + " chunks"; + + + double chunksPerSec = this.getEstimatedChunksPerSecond(); + if (chunksPerSec > 0) + { + long estimatedRemainingTime = (long) (remainingChunkCount / chunksPerSec); + message += " Estimated Time: " + formatSeconds(estimatedRemainingTime); // at " + F3Screen.NUMBER_FORMAT.format(chunksPerSec) + " chunks/sec"; + } + + + if (remainingChunkCount != 0) + { + EDhApiDistantGeneratorProgressDisplayLocation displayLocation = Config.Common.WorldGenerator.showGenerationProgress.get(); + if (displayLocation == EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY) + { + ClientApi.INSTANCE.showOverlayMessageNextFrame(message); + } + else if (displayLocation == EDhApiDistantGeneratorProgressDisplayLocation.CHAT) + { + ClientApi.INSTANCE.showChatMessageNextFrame(message); + } + else if (displayLocation == EDhApiDistantGeneratorProgressDisplayLocation.LOG) + { + LOGGER.info(message); + } + + } + } + private static String formatSeconds(long totalSeconds) + { + long days = totalSeconds / (24 * 3600); // 24 hours in a day + long hours = (totalSeconds % (24 * 3600)) / 3600; // Hours + long minutes = (totalSeconds % 3600) / 60; // Minutes + long seconds = totalSeconds % 60; // Seconds + + + String timeString = ""; + if (days > 0) + { + timeString += days+" "; + } + if (hours > 0) + { + timeString += hours+":"; + } + if (minutes > 0) + { + timeString += String.format("%02d", minutes)+":"; + } + timeString += String.format("%02d", seconds); + + return timeString; + } + + /** @return -1 if this method isn't supported or available */ + public double getEstimatedChunksPerSecond() + { + RollingAverage avg = this.worldGenerationQueue.getRollingAverageChunkGenTimeInMs(); + if (avg == null) + { + return -1; + } + + + // convert chunk generation time in milliseconds to chunks per second + double chunksPerSecond = (1 / avg.getAverage()) * 1_000; + // estimate the number of chunks that can be processed per second by all threads + // Note: this is probably higher than the actual number, we might want to drop this by 1 or 2 to give a more realistic estimate + chunksPerSecond = ThreadPoolUtil.getThreadCount() * chunksPerSecond; + + return chunksPerSecond; + } + } + + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/logging/f3/F3Screen.java b/core/src/main/java/com/seibel/distanthorizons/core/logging/f3/F3Screen.java index 0ee693657..4cccb3f6c 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/logging/f3/F3Screen.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/logging/f3/F3Screen.java @@ -123,7 +123,7 @@ public class F3Screen if (Config.Client.Advanced.Debugging.F3Screen.showThreadPools.get()) { // multi thread pools - messageList.add(getThreadPoolStatString("World Gen/Pull Chunk", worldGenPool));//"World Gen Tasks: 40/5304, (in progress: 7)"); + messageList.add(getThreadPoolStatString("World Gen/Import", worldGenPool)); messageList.add(getThreadPoolStatString("File Handler", fileHandlerPool)); messageList.add(getThreadPoolStatString("Update Propagator", updatePool)); messageList.add(getThreadPoolStatString("LOD Builder", lodBuilderPool)); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java index 48f36c70d..b396d6a79 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/pos/DhSectionPos.java @@ -224,7 +224,9 @@ public class DhSectionPos /** @return how wide this section is in blocks */ public static int getBlockWidth(long pos) { return BitShiftUtil.powerOfTwo(getDetailLevel(pos)); } - + /** @return how wide this section is in chunks */ + public static int getChunkWidth(long pos) { return DhSectionPos.getBlockWidth(pos) / LodUtil.CHUNK_WIDTH; } + public static DhBlockPos2D getCenterBlockPos(long pos) { return new DhBlockPos2D(getCenterBlockPosX(pos), getCenterBlockPosZ(pos)); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java b/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java index 132c48177..69eadd346 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java @@ -628,22 +628,33 @@ public class LodQuadTree extends QuadTree implements IDebugRen renderSection.tryQueuingMissingLodRetrieval(); } - // calculate an estimate for the max number of tasks for the queue - int totalWorldGenCount = 0; + // calculate an estimate for the max number of chunks for the queue + int totalWorldGenChunkCount = 0; + int totalWorldGenTaskCount = 0; for (int i = 0; i < nodeList.size(); i++) { LodRenderSection renderSection = nodeList.get(i); if (!renderSection.missingPositionsCalculated()) { - // may be higher than the actual amount - totalWorldGenCount += this.fullDataSourceProvider.getMaxPossibleRetrievalPositionCountForPos(renderSection.pos); + // chunk count + int sectionWidthInChunks = DhSectionPos.getChunkWidth(renderSection.pos); + totalWorldGenChunkCount += sectionWidthInChunks * sectionWidthInChunks; + + // task count + totalWorldGenTaskCount += renderSection.ungeneratedPositionCount(); } else { - totalWorldGenCount += renderSection.ungeneratedPositionCount(); + totalWorldGenChunkCount += renderSection.ungeneratedChunkCount(); + + // 1 since we assume the position can be generated in a single go + // TODO this is a bad assumption, can we determine what the world gen supports and determine it from that? + totalWorldGenTaskCount += 1; } } - this.fullDataSourceProvider.setTotalRetrievalPositionCount(totalWorldGenCount); + + this.fullDataSourceProvider.setEstimatedRemainingRetrievalChunkCount(totalWorldGenChunkCount); + this.fullDataSourceProvider.setTotalRetrievalPositionCount(totalWorldGenTaskCount); } catch (Exception e) { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java b/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java index 555dd60e9..62008245e 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java @@ -330,6 +330,23 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable LongArrayList missingGenerationPos = this.getMissingGenerationPos(); return missingGenerationPos != null ? missingGenerationPos.size() : 0; } + public int ungeneratedChunkCount() + { + LongArrayList missingGenerationPos = this.getMissingGenerationPos(); + if (missingGenerationPos == null) + { + return 0; + } + + int chunkCount = 0; + // get the number of chunks each position contains + for (int i = 0; i < missingGenerationPos.size(); i++) + { + int chunkWidth = DhSectionPos.getChunkWidth(missingGenerationPos.getLong(i)); + chunkCount += (chunkWidth * chunkWidth); + } + return chunkCount; + } public void tryQueuingMissingLodRetrieval() { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java index fcb22b01e..ea6b5f589 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java @@ -100,6 +100,8 @@ public interface IMinecraftClientWrapper extends IBindable ArrayList getAllServerWorlds(); void sendChatMessage(String string); + /** Will default to sending a chat message if not supported by the current MC version */ + void sendOverlayMessage(String string); /** Sends the given message to chat with a formatted prefix and color based on the log level. */ default void logToChat(Level logLevel, String message) diff --git a/core/src/main/resources/assets/distanthorizons/lang/en_us.json b/core/src/main/resources/assets/distanthorizons/lang/en_us.json index 0f896ad13..f98ef3b3e 100644 --- a/core/src/main/resources/assets/distanthorizons/lang/en_us.json +++ b/core/src/main/resources/assets/distanthorizons/lang/en_us.json @@ -567,15 +567,19 @@ "distanthorizons.config.common.worldGenerator.enableDistantGeneration": "Enable Distant Generation", "distanthorizons.config.common.worldGenerator.enableDistantGeneration.@tooltip": - "§6True:§r in single player LODs will be generated outside the vanilla render distance.\nNote: this can use a large amount of CPU.\n\n§6False:§r LODs will only generate within the vanilla render distance.", + "§6True:§r in single player LODs will be created outside the vanilla render distance.\nDepending on the Generator Mode pre-existing chunks will be important and/o\nmissing chunks will be generated.\nNote: this can use a large amount of CPU.\n\n§6False:§r LODs will only generate within the vanilla render distance.", "distanthorizons.config.common.worldGenerator.distantGeneratorMode": "Distance Generator Mode", "distanthorizons.config.common.worldGenerator.distantGeneratorMode.@tooltip": "How complicated the generation should be when generating LODs outside the vanilla render distance.", - "distanthorizons.config.common.worldGenerator.worldGenerationTimeoutLengthInSeconds": - "Timeout Length In Seconds", - "distanthorizons.config.common.worldGenerator.worldGenerationTimeoutLengthInSeconds.@tooltip": - "How long should a world generator thread run for before timing out? \nNote: If you are experiencing timeout errors it is better to lower your CPU usage first \nvia the thread config before changing this value.", + "distanthorizons.config.common.worldGenerator.showGenerationProgress": + "Show Generation Progress", + "distanthorizons.config.common.worldGenerator.showGenerationProgress.@tooltip": + "Determines how should distant generator progress be displayed.", + "distanthorizons.config.common.worldGenerator.generationProgressDisplayIntervalInSeconds": + "Progress Display Interval In Seconds", + "distanthorizons.config.common.worldGenerator.generationProgressDisplayIntervalInSeconds.@tooltip": + "Determines how long between progress update displays.", "distanthorizons.config.common.lodBuilding": @@ -877,6 +881,15 @@ "Features", "distanthorizons.config.enum.EDhApiDistantGeneratorMode.INTERNAL_SERVER": "Internal Server", + + "distanthorizons.config.enum.EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY": + "Overlay", + "distanthorizons.config.enum.EDhApiDistantGeneratorProgressDisplayLocation.CHAT": + "Chat", + "distanthorizons.config.enum.EDhApiDistantGeneratorProgressDisplayLocation.LOG": + "Log", + "distanthorizons.config.enum.EDhApiDistantGeneratorProgressDisplayLocation.DISABLED": + "Disabled", "distanthorizons.config.enum.EDhApiDataCompressionMode.UNCOMPRESSED": "Uncompressed",