diff --git a/src/main/java/net/neoforged/neoforge/server/command/ChunkGenWorker.java b/src/main/java/net/neoforged/neoforge/server/command/ChunkGenWorker.java deleted file mode 100644 index aee9adba93..0000000000 --- a/src/main/java/net/neoforged/neoforge/server/command/ChunkGenWorker.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.server.command; - -import java.util.ArrayDeque; -import java.util.Queue; -import net.minecraft.commands.CommandSourceStack; -import net.minecraft.core.BlockPos; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.ChunkStatus; -import net.neoforged.neoforge.common.WorldWorkerManager; - -public class ChunkGenWorker implements WorldWorkerManager.IWorker { - private final CommandSourceStack listener; - protected final BlockPos start; - protected final int total; - private final ServerLevel dim; - private final Queue queue; - private final int notificationFrequency; - private int lastNotification = 0; - private long lastNotifcationTime = 0; - private int genned = 0; - private Boolean keepingLoaded; - - public ChunkGenWorker(CommandSourceStack listener, BlockPos start, int total, ServerLevel dim, int interval) { - this.listener = listener; - this.start = start; - this.total = total; - this.dim = dim; - this.queue = buildQueue(); - this.notificationFrequency = interval != -1 ? interval : Math.max(total / 20, 100); //Every 5% or every 100, whichever is more. - this.lastNotifcationTime = System.currentTimeMillis(); //We also notify at least once every 60 seconds, to show we haven't froze. - } - - protected Queue buildQueue() { - Queue ret = new ArrayDeque(); - ret.add(start); - - //This *should* spiral outwards, starting on right side, down, left, up, right, but hey we'll see! - int radius = 1; - while (ret.size() < total) { - for (int q = -radius + 1; q <= radius && ret.size() < total; q++) - ret.add(start.offset(radius, 0, q)); - - for (int q = radius - 1; q >= -radius && ret.size() < total; q--) - ret.add(start.offset(q, 0, radius)); - - for (int q = radius - 1; q >= -radius && ret.size() < total; q--) - ret.add(start.offset(-radius, 0, q)); - - for (int q = -radius + 1; q <= radius && ret.size() < total; q++) - ret.add(start.offset(q, 0, -radius)); - - radius++; - } - return ret; - } - - public MutableComponent getStartMessage(CommandSourceStack sender) { - return Component.translatable("commands.neoforge.gen.start", total, start.getX(), start.getZ(), dim); - } - - @Override - public boolean hasWork() { - return queue.size() > 0; - } - - @Override - public boolean doWork() { - /* TODO: Check how many things are pending save, and slow down world gen if to many - AnvilChunkLoader loader = dim.getChunkProvider().chunkLoader instanceof AnvilChunkLoader ? (AnvilChunkLoader)world.getChunkProvider().chunkLoader : null; - if (loader != null && loader.getPendingSaveCount() > 100) - { - - if (lastNotifcationTime < System.currentTimeMillis() - 10*1000) - { - listener.sendFeedback(new TranslationTextComponent("commands.neoforge.gen.progress", total - queue.size(), total), true); - lastNotifcationTime = System.currentTimeMillis(); - } - return false; - } - */ - - BlockPos next = queue.poll(); - - if (next != null) { - // While we work we don't want to cause world load spam so pause unloading the world. - /* TODO: Readd if/when we introduce world unloading, or get Mojang to do it. - if (keepingLoaded == null) - keepingLoaded = DimensionManager.keepLoaded(dim, true); - */ - - if (++lastNotification >= notificationFrequency || lastNotifcationTime < System.currentTimeMillis() - 60 * 1000) { - listener.sendSuccess(() -> Component.translatable("commands.neoforge.gen.progress", total - queue.size(), total), true); - lastNotification = 0; - lastNotifcationTime = System.currentTimeMillis(); - } - - int x = next.getX(); - int z = next.getZ(); - - if (!dim.hasChunk(x, z)) { //Chunk is unloaded - ChunkAccess chunk = dim.getChunk(x, z, ChunkStatus.EMPTY, true); - if (!chunk.getStatus().isOrAfter(ChunkStatus.FULL)) { - chunk = dim.getChunk(x, z, ChunkStatus.FULL); - genned++; //There isn't a way to check if the chunk is actually created just if it was loaded - } - } - } - - if (queue.size() == 0) { - listener.sendSuccess(() -> Component.translatable("commands.neoforge.gen.complete", genned, total, dim.dimension().location()), true); - /* TODO: Readd if/when we introduce world unloading, or get Mojang to do it. - if (keepingLoaded != null && !keepingLoaded) - DimensionManager.keepLoaded(dim, false); - */ - return false; - } - return true; - } -} diff --git a/src/main/java/net/neoforged/neoforge/server/command/GenerateCommand.java b/src/main/java/net/neoforged/neoforge/server/command/GenerateCommand.java index 23b8fcbce0..2e5dc3725b 100644 --- a/src/main/java/net/neoforged/neoforge/server/command/GenerateCommand.java +++ b/src/main/java/net/neoforged/neoforge/server/command/GenerateCommand.java @@ -5,41 +5,149 @@ package net.neoforged.neoforge.server.command; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.BoolArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; -import net.minecraft.commands.arguments.DimensionArgument; import net.minecraft.commands.arguments.coordinates.BlockPosArgument; import net.minecraft.core.BlockPos; -import net.minecraft.server.level.ServerLevel; -import net.neoforged.neoforge.common.WorldWorkerManager; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.ChunkPos; +import net.neoforged.neoforge.server.command.generation.GenerationBar; +import net.neoforged.neoforge.server.command.generation.GenerationTask; +/** + * Special thanks to Jasmine and Gegy for allowing us to use their pregenerator mod as a model to use in NeoForge! + * Original code: https://github.com/jaskarth/fabric-chunkpregenerator + */ class GenerateCommand { + private static GenerationTask activeTask; + private static GenerationBar generationBar; + static ArgumentBuilder register() { - return Commands.literal("generate") - .requires(cs -> cs.hasPermission(4)) //permission + LiteralArgumentBuilder builder = Commands.literal("generate").requires(cs -> cs.hasPermission(4)); //permission + + builder.then(Commands.literal("start") .then(Commands.argument("pos", BlockPosArgument.blockPos()) - .then(Commands.argument("count", IntegerArgumentType.integer(1)) - .then(Commands.argument("dim", DimensionArgument.dimension()) - .then(Commands.argument("interval", IntegerArgumentType.integer()) - .executes(ctx -> execute(ctx.getSource(), BlockPosArgument.getSpawnablePos(ctx, "pos"), getInt(ctx, "count"), DimensionArgument.getDimension(ctx, "dim"), getInt(ctx, "interval")))) - .executes(ctx -> execute(ctx.getSource(), BlockPosArgument.getSpawnablePos(ctx, "pos"), getInt(ctx, "count"), DimensionArgument.getDimension(ctx, "dim"), -1))) - .executes(ctx -> execute(ctx.getSource(), BlockPosArgument.getSpawnablePos(ctx, "pos"), getInt(ctx, "count"), ctx.getSource().getLevel(), -1)))); + .then(Commands.argument("chunkRadius", IntegerArgumentType.integer(1, 1250)) // 20000 block radius limit + .then(Commands.argument("progressBar", BoolArgumentType.bool()) + .executes(ctx -> executeGeneration(ctx.getSource(), BlockPosArgument.getSpawnablePos(ctx, "pos"), getInt(ctx, "chunkRadius"), getBool(ctx, "progressBar")))) + .executes(ctx -> executeGeneration(ctx.getSource(), BlockPosArgument.getSpawnablePos(ctx, "pos"), getInt(ctx, "chunkRadius"), true))))); + + builder.then(Commands.literal("stop") + .executes(ctx -> stopGeneration(ctx.getSource()))); + + builder.then(Commands.literal("status") + .executes(ctx -> getGenerationStatus(ctx.getSource()))); + + builder.then(Commands.literal("help") + .executes(ctx -> getGenerationHelp(ctx.getSource()))); + + return builder; } private static int getInt(CommandContext ctx, String name) { return IntegerArgumentType.getInteger(ctx, name); } - private static int execute(CommandSourceStack source, BlockPos pos, int count, ServerLevel dim, int interval) { - BlockPos chunkpos = new BlockPos(pos.getX() >> 4, 0, pos.getZ() >> 4); + private static boolean getBool(CommandContext ctx, String name) { + return BoolArgumentType.getBool(ctx, name); + } + + private static int executeGeneration(CommandSourceStack source, BlockPos pos, int chunkRadius, boolean progressBar) { + if (activeTask != null) { + source.sendSuccess(() -> Component.translatable("commands.neoforge.chunkgen.already_running"), true); + return Command.SINGLE_SUCCESS; + } + + ChunkPos origin = new ChunkPos(pos); + + activeTask = new GenerationTask(source.getLevel(), origin.x, origin.z, chunkRadius); + int diameter = chunkRadius * 2 + 1; + + if (progressBar) { + generationBar = new GenerationBar(); + + if (source.getEntity() instanceof ServerPlayer) { + generationBar.addPlayer(source.getPlayer()); + } + } + + source.sendSuccess(() -> Component.translatable("commands.neoforge.chunkgen.started", + activeTask.getTotalCount(), diameter, diameter, diameter * 16, diameter * 16), true); + + activeTask.run(createPregenListener(source)); + + return Command.SINGLE_SUCCESS; + } + + private static int stopGeneration(CommandSourceStack source) { + if (activeTask != null) { + activeTask.stop(); + + int count = activeTask.getOkCount() + activeTask.getErrorCount() + activeTask.getSkippedCount(); + int total = activeTask.getTotalCount(); + + double percent = (double) count / total * 100.0; + source.sendSuccess(() -> Component.translatable("commands.neoforge.chunkgen.stopped", count, total, percent), true); + + generationBar.close(); + generationBar = null; + activeTask = null; + } else { + source.sendSuccess(() -> Component.translatable("commands.neoforge.chunkgen.not_running"), false); + } + + return Command.SINGLE_SUCCESS; + } + + private static int getGenerationStatus(CommandSourceStack source) { + if (activeTask != null) { + int count = activeTask.getOkCount() + activeTask.getErrorCount() + activeTask.getSkippedCount(); + int total = activeTask.getTotalCount(); + + double percent = (double) count / total * 100.0; + source.sendSuccess(() -> Component.translatable("commands.neoforge.chunkgen.status", count, total, percent), true); + } else { + source.sendSuccess(() -> Component.translatable("commands.neoforge.chunkgen.not_running"), false); + } + + return Command.SINGLE_SUCCESS; + } + + private static int getGenerationHelp(CommandSourceStack source) { + source.sendSuccess(() -> Component.translatable("commands.neoforge.chunkgen.help_line"), false); + return Command.SINGLE_SUCCESS; + } + + private static GenerationTask.Listener createPregenListener(CommandSourceStack source) { + return new GenerationTask.Listener() { + @Override + public void update(int ok, int error, int skipped, int total) { + if (generationBar != null) { + generationBar.update(ok, error, skipped, total); + } + } + + @Override + public void complete(int error) { + source.sendSuccess(() -> Component.translatable("commands.neoforge.chunkgen.success"), true); - ChunkGenWorker worker = new ChunkGenWorker(source, chunkpos, count, dim, interval); - source.sendSuccess(() -> worker.getStartMessage(source), true); - WorldWorkerManager.addWorker(worker); + if (error > 0) { + source.sendFailure(Component.translatable("commands.neoforge.chunkgen.error")); + } - return 0; + if (generationBar != null) { + generationBar.close(); + generationBar = null; + } + activeTask = null; + } + }; } } diff --git a/src/main/java/net/neoforged/neoforge/server/command/generation/CoarseOnionIterator.java b/src/main/java/net/neoforged/neoforge/server/command/generation/CoarseOnionIterator.java new file mode 100644 index 0000000000..57c7585cd0 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/server/command/generation/CoarseOnionIterator.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.server.command.generation; + +import com.google.common.collect.AbstractIterator; +import java.util.Iterator; +import java.util.NoSuchElementException; +import net.minecraft.world.level.ChunkPos; + +/** + * Special thanks to Jasmine and Gegy for allowing us to use their pregenerator mod as a model to use in NeoForge! + * Original code: https://github.com/jaskarth/fabric-chunkpregenerator + */ +public class CoarseOnionIterator extends AbstractIterator { + private final int radius; + private final int cellSize; + + private final OnionIterator cells; + private CellIterator cell; + + public CoarseOnionIterator(int radius, int cellSize) { + this.radius = radius; + this.cellSize = cellSize; + + this.cells = new OnionIterator((radius + cellSize - 1) / cellSize); + } + + @Override + protected ChunkPos computeNext() { + OnionIterator cells = this.cells; + CellIterator cell = this.cell; + while (cell == null || !cell.hasNext()) { + if (!cells.hasNext()) { + return this.endOfData(); + } + + ChunkPos cellPos = cells.next(); + this.cell = cell = this.createCellIterator(cellPos); + } + + return cell.next(); + } + + private CellIterator createCellIterator(ChunkPos pos) { + int size = this.cellSize; + int radius = this.radius; + + int x0 = pos.x * size; + int z0 = pos.z * size; + int x1 = x0 + size - 1; + int z1 = z0 + size - 1; + return new CellIterator( + Math.max(x0, -radius), Math.max(z0, -radius), + Math.min(x1, radius), Math.min(z1, radius)); + } + + private static final class CellIterator implements Iterator { + private final int x0; + private final int x1; + private final int z1; + + private int x; + private int z; + + private CellIterator(int x0, int z0, int x1, int z1) { + this.x = x0; + this.z = z0; + this.x0 = x0; + this.x1 = x1; + this.z1 = z1; + } + + @Override + public boolean hasNext() { + return this.z <= this.z1; + } + + @Override + public ChunkPos next() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + + ChunkPos pos = new ChunkPos(this.x, this.z); + if (this.x++ >= this.x1) { + this.x = this.x0; + this.z++; + } + + return pos; + } + } +} diff --git a/src/main/java/net/neoforged/neoforge/server/command/generation/GenerationBar.java b/src/main/java/net/neoforged/neoforge/server/command/generation/GenerationBar.java new file mode 100644 index 0000000000..73c7a22156 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/server/command/generation/GenerationBar.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.server.command.generation; + +import java.text.DecimalFormat; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.server.level.ServerBossEvent; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.BossEvent; + +/** + * Special thanks to Jasmine and Gegy for allowing us to use their pregenerator mod as a model to use in NeoForge! + * Original code: https://github.com/jaskarth/fabric-chunkpregenerator + */ +public class GenerationBar implements AutoCloseable { + private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("#.00"); + + private final ServerBossEvent bar; + + public GenerationBar() { + this.bar = new ServerBossEvent(Component.translatable("commands.neoforge.chunkgen.progress_bar_title"), BossEvent.BossBarColor.YELLOW, BossEvent.BossBarOverlay.PROGRESS); + this.bar.setPlayBossMusic(false); + this.bar.setCreateWorldFog(false); + this.bar.setDarkenScreen(false); + } + + public void update(int ok, int error, int skipped, int total) { + int count = ok + error + skipped; + + float percent = (float) count / total; + + MutableComponent title = Component.translatable("commands.neoforge.chunkgen.progress_bar_progress", total) + .append(Component.translatable(PERCENT_FORMAT.format(percent * 100.0F) + "%") + .setStyle(Style.EMPTY.withColor(ChatFormatting.GOLD))); + + if (error > 0) { + title = title.append(Component.translatable("commands.neoforge.chunkgen.progress_bar_errors") + .setStyle(Style.EMPTY.withColor(ChatFormatting.RED))); + } + + this.bar.setName(title); + this.bar.setProgress(percent); + } + + public void addPlayer(ServerPlayer player) { + this.bar.addPlayer(player); + } + + @Override + public void close() { + this.bar.setVisible(false); + this.bar.removeAllPlayers(); + } +} diff --git a/src/main/java/net/neoforged/neoforge/server/command/generation/GenerationTask.java b/src/main/java/net/neoforged/neoforge/server/command/generation/GenerationTask.java new file mode 100644 index 0000000000..733cf5cd88 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/server/command/generation/GenerationTask.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.server.command.generation; + +import com.mojang.datafixers.util.Either; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.visitors.CollectFields; +import net.minecraft.nbt.visitors.FieldSelector; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Special thanks to Jasmine and Gegy for allowing us to use their pregenerator mod as a model to use in NeoForge! + * Original code: https://github.com/jaskarth/fabric-chunkpregenerator + */ +public class GenerationTask { + private static final Logger LOGGER = LogManager.getLogger(); + private static final int BATCH_SIZE = 32; + private static final int QUEUE_THRESHOLD = 8; + private static final int COARSE_CELL_SIZE = 4; + + private final MinecraftServer server; + private final ServerChunkCache chunkSource; + + private final Iterator iterator; + private final int x; + private final int z; + private final int radius; + + private final int totalCount; + + private final Object queueLock = new Object(); + + private final AtomicInteger queuedCount = new AtomicInteger(); + private final AtomicInteger okCount = new AtomicInteger(); + private final AtomicInteger errorCount = new AtomicInteger(); + private final AtomicInteger skippedCount = new AtomicInteger(); + + private volatile Listener listener; + private volatile boolean stopped; + + public static final TicketType NEOFORGE_GENERATE_FORCED = TicketType.create("neoforge_generate_forced", Comparator.comparingLong(ChunkPos::toLong)); + + public GenerationTask(ServerLevel serverLevel, int x, int z, int radius) { + this.server = serverLevel.getServer(); + this.chunkSource = serverLevel.getChunkSource(); + + this.iterator = new CoarseOnionIterator(radius, COARSE_CELL_SIZE); + this.x = x; + this.z = z; + this.radius = radius; + + int diameter = radius * 2 + 1; + this.totalCount = diameter * diameter; + } + + public int getOkCount() { + return this.okCount.get(); + } + + public int getErrorCount() { + return this.errorCount.get(); + } + + public int getSkippedCount() { + return this.skippedCount.get(); + } + + public int getTotalCount() { + return this.totalCount; + } + + public void run(Listener listener) { + if (this.listener != null) { + throw new IllegalStateException("already running!"); + } + + this.listener = listener; + + this.server.submit(() -> CompletableFuture.runAsync(this::tryEnqueueTasks)); + } + + public void stop() { + synchronized (this.queueLock) { + this.stopped = true; + this.listener = null; + } + } + + private void tryEnqueueTasks() { + synchronized (this.queueLock) { + if (this.stopped) { + return; + } + + int enqueueCount = BATCH_SIZE - this.queuedCount.get(); + if (enqueueCount <= 0) { + return; + } + + LongList chunks = this.collectChunks(enqueueCount); + if (chunks.isEmpty()) { + this.listener.complete(this.errorCount.get()); + this.stopped = true; + return; + } + + this.queuedCount.getAndAdd(chunks.size()); + this.server.submit(() -> this.enqueueChunks(chunks)); + } + } + + private void enqueueChunks(LongList chunks) { + for (int i = 0; i < chunks.size(); i++) { + long chunk = chunks.getLong(i); + this.acquireChunk(chunk); + } + + // tick the chunk manager to force the ChunkHolders to be created + this.chunkSource.tick(() -> false, true); + + ChunkMap chunkMap = this.chunkSource.chunkMap; + + for (int i = 0; i < chunks.size(); i++) { + long chunkLongPos = chunks.getLong(i); + + ChunkHolder holder = chunkMap.getVisibleChunkIfPresent(chunkLongPos); + if (holder == null) { + LOGGER.warn("Added ticket for chunk but it was not added! ({}; {})", ChunkPos.getX(chunkLongPos), ChunkPos.getZ(chunkLongPos)); + this.acceptChunkResult(chunkLongPos, ChunkHolder.UNLOADED_CHUNK); + continue; + } + + holder.getOrScheduleFuture(ChunkStatus.FULL, chunkMap).whenComplete((result, throwable) -> { + if (throwable == null) { + this.acceptChunkResult(chunkLongPos, result); + } else { + LOGGER.warn("Encountered unexpected error while generating chunk", throwable); + this.acceptChunkResult(chunkLongPos, ChunkHolder.UNLOADED_CHUNK); + } + }); + } + } + + private void acceptChunkResult(long chunk, Either result) { + this.server.submit(() -> this.releaseChunk(chunk)); + + if (result.left().isPresent()) { + this.okCount.getAndIncrement(); + } else { + this.errorCount.getAndIncrement(); + } + + this.listener.update(this.okCount.get(), this.errorCount.get(), this.skippedCount.get(), this.totalCount); + + int queuedCount = this.queuedCount.decrementAndGet(); + if (queuedCount <= QUEUE_THRESHOLD) { + this.tryEnqueueTasks(); + } + } + + private LongList collectChunks(int count) { + LongList chunks = new LongArrayList(count); + + Iterator iterator = this.iterator; + int i = 0; + while (i < count && iterator.hasNext()) { + ChunkPos chunkPosInLocalSpace = iterator.next(); + if (Math.abs(chunkPosInLocalSpace.x) <= this.radius && Math.abs(chunkPosInLocalSpace.z) <= this.radius) { + if (isChunkFullyGenerated(chunkPosInLocalSpace)) { + this.skippedCount.incrementAndGet(); + this.listener.update(this.okCount.get(), this.errorCount.get(), this.skippedCount.get(), this.totalCount); + continue; + } + + chunks.add(ChunkPos.asLong(chunkPosInLocalSpace.x + this.x, chunkPosInLocalSpace.z + this.z)); + i++; + } + } + + return chunks; + } + + private void acquireChunk(long chunk) { + ChunkPos pos = new ChunkPos(chunk); + this.chunkSource.addRegionTicket(NEOFORGE_GENERATE_FORCED, pos, 0, pos); + } + + private void releaseChunk(long chunk) { + ChunkPos pos = new ChunkPos(chunk); + this.chunkSource.addRegionTicket(NEOFORGE_GENERATE_FORCED, pos, 0, pos); + } + + private boolean isChunkFullyGenerated(ChunkPos chunkPosInLocalSpace) { + ChunkPos chunkPosInWorldSpace = new ChunkPos(chunkPosInLocalSpace.x + this.x, chunkPosInLocalSpace.z + this.z); + CollectFields collectFields = new CollectFields(new FieldSelector(StringTag.TYPE, "Status")); + this.chunkSource.chunkMap.chunkScanner().scanChunk(chunkPosInWorldSpace, collectFields).join(); + + if (collectFields.getResult() instanceof CompoundTag compoundTag) { + return compoundTag.getString("Status").equals("minecraft:full"); + } + + return false; + } + + public interface Listener { + void update(int ok, int error, int skipped, int total); + + void complete(int error); + } +} diff --git a/src/main/java/net/neoforged/neoforge/server/command/generation/OnionIterator.java b/src/main/java/net/neoforged/neoforge/server/command/generation/OnionIterator.java new file mode 100644 index 0000000000..d416d5291a --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/server/command/generation/OnionIterator.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.server.command.generation; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import net.minecraft.world.level.ChunkPos; + +/** + * Special thanks to Jasmine and Gegy for allowing us to use their pregenerator mod as a model to use in NeoForge! + * Original code: https://github.com/jaskarth/fabric-chunkpregenerator + */ +public class OnionIterator implements Iterator { + private static final byte EAST = 0; + private static final byte SOUTH = 1; + private static final byte WEST = 2; + private static final byte NORTH = 3; + private static final byte STOP = 4; + + private final int radius; + + private int x; + private int z; + + private int distance = 0; + private byte state = EAST; + + public OnionIterator(int radius) { + this.radius = radius; + } + + @Override + public ChunkPos next() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + + ChunkPos pos = new ChunkPos(this.x, this.z); + + switch (this.state) { + case EAST: + if (++this.x >= this.distance) { + this.state = SOUTH; + if (this.distance > this.radius) { + this.state = STOP; + } + } + break; + case SOUTH: + if (++this.z >= this.distance) { + this.state = WEST; + } + break; + case WEST: + if (--this.x <= -this.distance) { + this.state = NORTH; + } + break; + case NORTH: + if (--this.z <= -this.distance) { + this.state = EAST; + this.distance++; + } + break; + } + + if (this.distance == 0) { + this.distance++; + } + + return pos; + } + + @Override + public boolean hasNext() { + return this.state != STOP; + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 39edd30780..efa0d24086 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -256,6 +256,7 @@ public net.minecraft.resources.ResourceLocation isValidNamespace(Ljava/lang/Stri protected net.minecraft.server.MinecraftServer nextTickTimeNanos # nextTickTimeNanos public net.minecraft.server.MinecraftServer$ReloadableResources public net.minecraft.server.dedicated.DedicatedServer consoleInput # consoleInput +public net.minecraft.server.level.ChunkMap getVisibleChunkIfPresent(J)Lnet/minecraft/server/level/ChunkHolder; public net.minecraft.server.level.ServerChunkCache level # level public net.minecraft.server.level.ServerLevel getEntities()Lnet/minecraft/world/level/entity/LevelEntityGetter; # getEntities public net.minecraft.server.level.ServerPlayer containerCounter # containerCounter diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index 2f62e5c9c7..577ebd624a 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -113,11 +113,6 @@ "commands.neoforge.entity.list.none": "No entities found.", "commands.neoforge.entity.list.single.header": "Entity: {0} Total: {1}", "commands.neoforge.entity.list.multiple.header": "Total: {0}", - "commands.neoforge.gen.usage": "Use /neoforge gen [dimension] [interval]", - "commands.neoforge.gen.dim_fail": "Failed to load world for dimension {0}, Task terminated.", - "commands.neoforge.gen.progress": "Generation Progress: {0}/{1}", - "commands.neoforge.gen.complete": "Finished generating {0} new chunks (out of {1}) for dimension {2}.", - "commands.neoforge.gen.start": "Starting to generate {0} chunks in a spiral around {1}, {2} in dimension {3}.", "commands.neoforge.setdim.invalid.entity": "The entity selected ({0}) is not valid.", "commands.neoforge.setdim.invalid.dim": "The dimension ID specified ({0}) is not valid.", "commands.neoforge.setdim.invalid.nochange": "The entity selected ({0}) is already in the dimension specified ({1}).", @@ -146,6 +141,17 @@ "commands.neoforge.tags.containing_tag_count": "Containing tags: %s", "commands.neoforge.tags.element": "%s : %s", "commands.neoforge.tags.page_info": "%s ", + "commands.neoforge.chunkgen.progress_bar_title": "Generating chunks...", + "commands.neoforge.chunkgen.progress_bar_progress": "Generating {0} chunks - ", + "commands.neoforge.chunkgen.progress_bar_errors": "({0} errors!)", + "commands.neoforge.chunkgen.already_running": "Generation already running. Please execute '/neoforge generate stop' first and then you can start a new generation.", + "commands.neoforge.chunkgen.started": "Generating {0} chunks, in an area of {1}x{2} chunks ({3}x{4} blocks).", + "commands.neoforge.chunkgen.success": "Generation Done!", + "commands.neoforge.chunkgen.error": "Generation experienced {0} errors! Check the log for more information.", + "commands.neoforge.chunkgen.stopped": "Generation stopped! {0} out of {1} chunks generated. ({2}%)", + "commands.neoforge.chunkgen.status": "Generation status! {0} out of {1} chunks generated. ({2}%)", + "commands.neoforge.chunkgen.not_running": "No pregeneration currently running. Run `/neoforge generate help` to see commands for starting generation.", + "commands.neoforge.chunkgen.help_line": "§2/neoforge generate start [progressBar] §r§f- Generates a square centered on the given position that is chunkRadius * 2 on each side.\n§2/neoforge generate stop §r§f- Stops the current generation and displays progress that it had completed.\n§2/neoforge generate status §r- Displays the progress completed for the currently running generation.\n§2/neoforge generate help §r- Displays this message.\nGeneral tips: If running from a server console, you can run generate in different dimensions by using /execute in neoforge generate...", "commands.config.getwithtype": "Config for %s of type %s found at %s", "commands.config.noconfig": "Config for %s of type %s not found",