Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group = "fr.sukikui.biomemap"
version = "0.1.3"
version = "0.1.4"

def paperApiVersion = '1.21.11-R0.1-SNAPSHOT'
def hyphenIndex = paperApiVersion.indexOf('-')
Expand Down
36 changes: 5 additions & 31 deletions src/main/java/fr/sukikui/biomemap/command/BiomeMapCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import fr.sukikui.biomemap.export.AsyncBiomeExportTask;
import fr.sukikui.biomemap.export.BiomeExporter;
import fr.sukikui.biomemap.util.ProgressFormatter;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
Expand Down Expand Up @@ -37,7 +38,6 @@ public final class BiomeMapCommand implements CommandExecutor, TabCompleter {
private static final int PLAYER_STATUS_MAP_MAX_HEIGHT = 10;
private static final int CONSOLE_STATUS_MAP_MAX_WIDTH = 56;
private static final int CONSOLE_STATUS_MAP_MAX_HEIGHT = 18;
private static final int STATUS_BAR_WIDTH = 20;

private final JavaPlugin plugin;
private final BiomeExporter exporter;
Expand Down Expand Up @@ -343,19 +343,15 @@ private boolean handleStatusCommand(CommandSender sender, String[] args) {
status.gridHeight(),
status.totalCells()));

String eta = status.etaMs() < 0 ? "n/a" : formatDuration(status.etaMs());
String progressBar = buildProgressBar(status.progressPercent(), STATUS_BAR_WIDTH);
sendInfo(
sender,
String.format(
Locale.ROOT,
"Progress=§f%s§7 §f%.1f%%§7 chunks=§f%d/%d§7 elapsed=§f%s§7 eta=§f%s",
progressBar,
ProgressFormatter.formatChatLine(
status.progressPercent(),
status.completedChunks(),
status.totalChunks(),
formatDuration(status.elapsedMs()),
eta));
status.elapsedMs(),
status.etaMs(),
ProgressFormatter.DEFAULT_BAR_WIDTH));

if (status.initiatorName() != null && !status.initiatorName().isBlank()) {
String initiatorState = status.initiatorOnline() ? "online" : "offline";
Expand Down Expand Up @@ -449,28 +445,6 @@ private void sendError(CommandSender sender, String message) {
sender.sendMessage(CHAT_PREFIX + "§c§lError: §c" + message);
}

private String formatDuration(long durationMs) {
long totalSeconds = Math.max(0L, durationMs / 1000L);
long hours = totalSeconds / 3600L;
long minutes = (totalSeconds % 3600L) / 60L;
long seconds = totalSeconds % 60L;
if (hours > 0) {
return String.format(Locale.ROOT, "%dh %02dm %02ds", hours, minutes, seconds);
}
if (minutes > 0) {
return String.format(Locale.ROOT, "%dm %02ds", minutes, seconds);
}
return String.format(Locale.ROOT, "%ds", seconds);
}

private String buildProgressBar(double percent, int width) {
int safeWidth = Math.max(1, width);
double bounded = Math.max(0.0, Math.min(100.0, percent));
int filled = (int) Math.round((bounded / 100.0) * safeWidth);
filled = Math.max(0, Math.min(safeWidth, filled));
return "[" + "#".repeat(filled) + "-".repeat(safeWidth - filled) + "]";
}

private String toLogPath(File file) {
Path absolute = file.toPath().toAbsolutePath().normalize();
Path serverRoot = plugin.getServer().getWorldContainer().toPath().toAbsolutePath().normalize();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import fr.sukikui.biomemap.export.BiomeExporter.BiomeCell;
import fr.sukikui.biomemap.export.BiomeExporter.BiomeMapExport;
import fr.sukikui.biomemap.export.BiomeExporter.Point;
import fr.sukikui.biomemap.util.ProgressFormatter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
Expand Down Expand Up @@ -43,6 +44,7 @@ public final class AsyncBiomeExportTask extends BukkitRunnable {

private static final int CHUNK_SIZE = 16;
private static final String CHAT_PREFIX = "§8[§b§lBiomeMap§8] §r";
private static final long FIRST_PROGRESS_HEARTBEAT_MS = TimeUnit.SECONDS.toMillis(10);
private static final long PROGRESS_HEARTBEAT_MS = TimeUnit.MINUTES.toMillis(5);
private static final double EPSILON = 0.000001;

Expand Down Expand Up @@ -90,6 +92,7 @@ public final class AsyncBiomeExportTask extends BukkitRunnable {
private final AtomicBoolean finishing = new AtomicBoolean(false);
private final AtomicBoolean stopRequested = new AtomicBoolean(false);
private final AtomicBoolean completionNotified = new AtomicBoolean(false);
private boolean hasProgressLog = false;
private final long startTimeMs = System.currentTimeMillis();
private long lastProgressLogAtMs = startTimeMs;
private volatile File outputPreviewFile;
Expand Down Expand Up @@ -344,40 +347,56 @@ private void handleChunkCompletion(ChunkCompletion completion) {
}
}

private void reportProgress(long completedChunks) {
double percent = (completedChunks * 100.0) / totalChunks;
sendInfo(String.format(
Locale.ROOT,
"Progress §f§l%.1f%%§7 (§f%d/%d§7 chunks)",
percent,
completedChunks,
totalChunks));
private void reportProgress(ProgressFormatter.ProgressSnapshot progress) {
sendInfo(
ProgressFormatter.formatChatLine(
progress.progressPercent(),
progress.completedChunks(),
progress.totalChunks(),
progress.elapsedMs(),
progress.etaMs(),
ProgressFormatter.DEFAULT_BAR_WIDTH));
if (!isConsoleSender()) {
String plainLine = String.format(
Locale.ROOT, "Progress %.1f%% (%d/%d chunks)", percent, completedChunks, totalChunks);
logger.info(plainLine);
logger.info(
ProgressFormatter.formatPlainLine(
progress.progressPercent(),
progress.completedChunks(),
progress.totalChunks(),
progress.elapsedMs(),
progress.etaMs(),
ProgressFormatter.DEFAULT_BAR_WIDTH));
}
}

private void maybeReportProgress(long completedChunks, boolean timeoutCheck) {
if (totalChunks <= 0) {
return;
}
double percent = (completedChunks * 100.0) / totalChunks;
long elapsedMs = System.currentTimeMillis() - startTimeMs;
ProgressFormatter.ProgressSnapshot progress =
ProgressFormatter.calculate(completedChunks, totalChunks, elapsedMs);
double percent = progress.progressPercent();
boolean reachedThreshold = false;
while (nextProgressPercent <= 100 && percent + EPSILON >= nextProgressPercent) {
reachedThreshold = true;
nextProgressPercent += 10;
}
boolean completed = completedChunks >= totalChunks;
long now = System.currentTimeMillis();
boolean timedOut = timeoutCheck
boolean firstHeartbeatDue = timeoutCheck
&& !completed
&& !hasProgressLog
&& (now - startTimeMs) >= FIRST_PROGRESS_HEARTBEAT_MS;
boolean regularHeartbeatDue = timeoutCheck
&& !completed
&& hasProgressLog
&& (now - lastProgressLogAtMs) >= PROGRESS_HEARTBEAT_MS;
boolean timedOut = firstHeartbeatDue || regularHeartbeatDue;
if (!reachedThreshold && !completed && !timedOut) {
return;
}
reportProgress(completedChunks);
reportProgress(progress);
hasProgressLog = true;
lastProgressLogAtMs = now;
}

Expand Down Expand Up @@ -626,11 +645,8 @@ private CommandSender resolveCurrentRecipient() {
public ExportStatus snapshotStatus() {
long completedChunksSnapshot = chunksCompleted.get();
long elapsedMs = System.currentTimeMillis() - startTimeMs;
long etaMs = -1L;
if (completedChunksSnapshot > 0 && completedChunksSnapshot < totalChunks) {
double msPerChunk = elapsedMs / (double) completedChunksSnapshot;
etaMs = (long) (msPerChunk * (totalChunks - completedChunksSnapshot));
}
ProgressFormatter.ProgressSnapshot progress =
ProgressFormatter.calculate(completedChunksSnapshot, totalChunks, elapsedMs);

String previewPath = null;
if (previewEnabled) {
Expand Down Expand Up @@ -671,9 +687,9 @@ public ExportStatus snapshotStatus() {
chunkRows,
completedChunksSnapshot,
totalChunks,
totalChunks <= 0 ? 100.0 : (completedChunksSnapshot * 100.0) / totalChunks,
elapsedMs,
etaMs,
progress.progressPercent(),
progress.elapsedMs(),
progress.etaMs(),
toLogPath(outputFile),
previewPath,
initiatorName,
Expand Down
129 changes: 129 additions & 0 deletions src/main/java/fr/sukikui/biomemap/util/ProgressFormatter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package fr.sukikui.biomemap.util;

import java.util.Locale;

/**
* Shared helpers to compute and render export progress consistently.
*/
public final class ProgressFormatter {

public static final int DEFAULT_BAR_WIDTH = 20;

private ProgressFormatter() {
}

/**
* Computes percentage and ETA from chunk counters and elapsed duration.
*/
public static ProgressSnapshot calculate(long completedChunks, long totalChunks, long elapsedMs) {
long safeCompletedChunks = Math.max(0L, completedChunks);
long safeTotalChunks = Math.max(0L, totalChunks);
long safeElapsedMs = Math.max(0L, elapsedMs);

double progressPercent;
if (safeTotalChunks <= 0L) {
progressPercent = 100.0;
} else {
progressPercent = (safeCompletedChunks * 100.0) / safeTotalChunks;
}
progressPercent = Math.max(0.0, Math.min(100.0, progressPercent));

long etaMs = -1L;
if (safeCompletedChunks > 0L && safeCompletedChunks < safeTotalChunks) {
double msPerChunk = safeElapsedMs / (double) safeCompletedChunks;
etaMs = (long) (msPerChunk * (safeTotalChunks - safeCompletedChunks));
}

return new ProgressSnapshot(
safeCompletedChunks,
safeTotalChunks,
progressPercent,
safeElapsedMs,
etaMs);
}

/**
* Renders the standard colored chat progress line.
*/
public static String formatChatLine(
double progressPercent,
long completedChunks,
long totalChunks,
long elapsedMs,
long etaMs,
int barWidth) {
return String.format(
Locale.ROOT,
"Progress=§f%s§7 §f%.1f%%§7 chunks=§f%d/%d§7 elapsed=§f%s§7 eta=§f%s",
buildProgressBar(progressPercent, barWidth),
progressPercent,
completedChunks,
totalChunks,
formatDuration(elapsedMs),
formatEta(etaMs));
}

/**
* Renders the plain progress line for logger output.
*/
public static String formatPlainLine(
double progressPercent,
long completedChunks,
long totalChunks,
long elapsedMs,
long etaMs,
int barWidth) {
return String.format(
Locale.ROOT,
"Progress=%s %.1f%% chunks=%d/%d elapsed=%s eta=%s",
buildProgressBar(progressPercent, barWidth),
progressPercent,
completedChunks,
totalChunks,
formatDuration(elapsedMs),
formatEta(etaMs));
}

/**
* Formats elapsed or remaining duration.
*/
public static String formatDuration(long durationMs) {
long totalSeconds = Math.max(0L, durationMs / 1000L);
long hours = totalSeconds / 3600L;
long minutes = (totalSeconds % 3600L) / 60L;
long seconds = totalSeconds % 60L;
if (hours > 0) {
return String.format(Locale.ROOT, "%dh %02dm %02ds", hours, minutes, seconds);
}
if (minutes > 0) {
return String.format(Locale.ROOT, "%dm %02ds", minutes, seconds);
}
return String.format(Locale.ROOT, "%ds", seconds);
}

/**
* Builds an ASCII progress bar from 0 to 100%.
*/
public static String buildProgressBar(double percent, int width) {
int safeWidth = Math.max(1, width);
double bounded = Math.max(0.0, Math.min(100.0, percent));
int filled = (int) Math.round((bounded / 100.0) * safeWidth);
filled = Math.max(0, Math.min(safeWidth, filled));
return "[" + "#".repeat(filled) + "-".repeat(safeWidth - filled) + "]";
}

private static String formatEta(long etaMs) {
return etaMs < 0 ? "n/a" : formatDuration(etaMs);
}

/**
* Immutable calculation payload for progress rendering.
*/
public record ProgressSnapshot(
long completedChunks,
long totalChunks,
double progressPercent,
long elapsedMs,
long etaMs) {
}
}
Loading