├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── src └── main │ ├── resources │ ├── chunkpregen.accesswidener │ ├── chunkpregen.mixins.json │ └── fabric.mod.json │ └── java │ └── com │ └── jaskarth │ └── chunkpregen │ ├── mixin │ ├── SerializingRegionBasedStorageAccessor.java │ ├── ThreadedAnvilChunkStorageAccessor.java │ └── ThreadedAnvilChunkStorageMixin.java │ ├── PregenBar.java │ ├── ChunkPregen.java │ ├── iterator │ ├── OnionIterator.java │ └── CoarseOnionIterator.java │ ├── Commands.java │ └── PregenerationTask.java ├── settings.gradle ├── gradle.properties ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaskarth/fabric-chunkpregenerator/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # idea 9 | 10 | .idea/ 11 | *.iml 12 | *.ipr 13 | *.iws 14 | 15 | # vscode 16 | 17 | .settings/ 18 | .vscode/ 19 | bin/ 20 | .classpath 21 | .project 22 | 23 | # fabric 24 | 25 | run/ 26 | logs/ -------------------------------------------------------------------------------- /src/main/resources/chunkpregen.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v1 named 2 | 3 | accessible method net/minecraft/server/world/ServerChunkLoadingManager getChunkHolder (J)Lnet/minecraft/server/world/ChunkHolder; 4 | 5 | # accessible method net/minecraft/server/world/ServerChunkManager tick ()Z 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | maven { 8 | name 'Cotton' 9 | url 'https://server.bbkr.space/artifactory/libs-release/' 10 | } 11 | gradlePluginPortal() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/chunkpregen.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "com.jaskarth.chunkpregen.mixin", 4 | "compatibilityLevel": "JAVA_8", 5 | "mixins": [ 6 | "SerializingRegionBasedStorageAccessor", 7 | "ThreadedAnvilChunkStorageAccessor", 8 | "ThreadedAnvilChunkStorageMixin" 9 | ], 10 | "client": [ 11 | ], 12 | "injectors": { 13 | "defaultRequire": 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | 4 | # Fabric Properties 5 | # check these on https://modmuss50.me/fabric.html 6 | minecraft_version=1.21.1 7 | yarn_mappings=1.21.1+build.3 8 | loader_version=0.16.5 9 | 10 | # Fabric API 11 | fabric_version=0.104.0+1.21.1 12 | 13 | # Mod Properties 14 | mod_version = 0.3.8 15 | maven_group = com.jaskarth.chunkpregen 16 | archives_base_name = fabric-chunkpregen -------------------------------------------------------------------------------- /src/main/java/com/jaskarth/chunkpregen/mixin/SerializingRegionBasedStorageAccessor.java: -------------------------------------------------------------------------------- 1 | package com.jaskarth.chunkpregen.mixin; 2 | 3 | import it.unimi.dsi.fastutil.longs.Long2ObjectMap; 4 | import net.minecraft.world.storage.SerializingRegionBasedStorage; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.gen.Accessor; 7 | 8 | import java.util.Optional; 9 | 10 | @Mixin(SerializingRegionBasedStorage.class) 11 | public interface SerializingRegionBasedStorageAccessor { 12 | @Accessor 13 | Long2ObjectMap> getLoadedElements(); 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # About 3 | 4 | Fabric Chunk Pregenerator is a fabric mod that allows you to pregenerate chunks for your server or for singleplayer while running fabric. Requires Fabric API! 5 | 6 | 7 | 8 | # Commands 9 | 10 | `/pregen start ` - Pregenerates in a square that is chunks long and wide. Only one pregeneration can run at a time. 11 | 12 | `/pregen stop` - Stops pregeneration and displays the amount completed. 13 | 14 | `/pregen status` - Displays the amount of chunks pregenerated. 15 | 16 | `/pregen help` - Displays a help message. 17 | 18 | 19 | 20 | ## Discord Server 21 | 22 | https://discord.gg/BuBGds9 -------------------------------------------------------------------------------- /src/main/java/com/jaskarth/chunkpregen/mixin/ThreadedAnvilChunkStorageAccessor.java: -------------------------------------------------------------------------------- 1 | package com.jaskarth.chunkpregen.mixin; 2 | 3 | import net.minecraft.server.world.ChunkTaskPrioritySystem; 4 | import net.minecraft.server.world.ServerChunkLoadingManager; 5 | import net.minecraft.util.thread.MessageListener; 6 | import net.minecraft.util.thread.ThreadExecutor; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.gen.Accessor; 9 | 10 | @Mixin(ServerChunkLoadingManager.class) 11 | public interface ThreadedAnvilChunkStorageAccessor { 12 | @Accessor 13 | MessageListener> getMainExecutor(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "chunkpregen", 4 | "version": "0.3.8", 5 | 6 | "name": "Fabric Chunk Pregenerator", 7 | "description": "Pregen chunks with fabric!", 8 | "authors": [ 9 | "jaskarth" 10 | ], 11 | "contact": { 12 | "sources": "" 13 | }, 14 | 15 | "license": "LGPLv3", 16 | "icon": "assets/modid/icon.png", 17 | 18 | "environment": "*", 19 | "entrypoints": { 20 | "main": [ 21 | "com.jaskarth.chunkpregen.ChunkPregen" 22 | ] 23 | }, 24 | 25 | "accessWidener": "chunkpregen.accesswidener", 26 | 27 | "mixins": [ 28 | "chunkpregen.mixins.json" 29 | ], 30 | 31 | "depends": { 32 | "fabricloader": ">=0.7.2", 33 | "fabric": "*", 34 | "minecraft": ">=1.14-" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/jaskarth/chunkpregen/mixin/ThreadedAnvilChunkStorageMixin.java: -------------------------------------------------------------------------------- 1 | package com.jaskarth.chunkpregen.mixin; 2 | 3 | import com.jaskarth.chunkpregen.ChunkPregen; 4 | import it.unimi.dsi.fastutil.longs.Long2ByteMap; 5 | import net.minecraft.server.world.ChunkHolder; 6 | import net.minecraft.server.world.ServerChunkLoadingManager; 7 | import net.minecraft.world.chunk.Chunk; 8 | import net.minecraft.world.chunk.WorldChunk; 9 | import net.minecraft.world.poi.PointOfInterestStorage; 10 | import org.spongepowered.asm.mixin.Final; 11 | import org.spongepowered.asm.mixin.Mixin; 12 | import org.spongepowered.asm.mixin.Shadow; 13 | import org.spongepowered.asm.mixin.injection.At; 14 | import org.spongepowered.asm.mixin.injection.Inject; 15 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 16 | 17 | import java.util.concurrent.CompletableFuture; 18 | 19 | @Mixin(ServerChunkLoadingManager.class) 20 | public class ThreadedAnvilChunkStorageMixin { 21 | 22 | @Final 23 | @Shadow 24 | private Long2ByteMap chunkToType; 25 | 26 | @Final 27 | @Shadow 28 | private PointOfInterestStorage pointOfInterestStorage; 29 | 30 | @Inject(method = "method_60440(Lnet/minecraft/server/world/ChunkHolder;J)V", 31 | at = @At(value = "INVOKE", target = "Lnet/minecraft/server/world/ServerChunkLoadingManager;save(Lnet/minecraft/world/chunk/Chunk;)Z")) 32 | private void fabricChunkPregenerator$unloadChunkPOI(ChunkHolder chunkHolder, long chunkLong, CallbackInfo ci) { 33 | this.chunkToType.remove(chunkLong); 34 | ChunkPregen.onChunkUnload(pointOfInterestStorage, chunkHolder.getLatest()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/jaskarth/chunkpregen/PregenBar.java: -------------------------------------------------------------------------------- 1 | package com.jaskarth.chunkpregen; 2 | 3 | import net.minecraft.entity.boss.BossBar; 4 | import net.minecraft.entity.boss.ServerBossBar; 5 | import net.minecraft.server.network.ServerPlayerEntity; 6 | import net.minecraft.text.MutableText; 7 | import net.minecraft.text.Text; 8 | import net.minecraft.util.Formatting; 9 | 10 | import java.text.DecimalFormat; 11 | 12 | public final class PregenBar implements AutoCloseable { 13 | private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("#.00"); 14 | 15 | private final ServerBossBar bar; 16 | 17 | public PregenBar() { 18 | this.bar = new ServerBossBar(Text.literal("Pregenerating"), BossBar.Color.BLUE, BossBar.Style.PROGRESS); 19 | this.bar.setDragonMusic(false); 20 | this.bar.setThickenFog(false); 21 | this.bar.setDarkenSky(false); 22 | } 23 | 24 | public void update(int ok, int error, int skipped, int total) { 25 | int count = ok + error + skipped; 26 | 27 | float percent = (float) count / total; 28 | 29 | MutableText title = Text.literal("Pregenerating " + total + " chunks! ") 30 | .append(Text.literal(PERCENT_FORMAT.format(percent * 100.0F) + "%").formatted(Formatting.AQUA)); 31 | 32 | if (error > 0) { 33 | title = title.append(Text.literal(" (" + error + " errors!)").formatted(Formatting.RED)); 34 | } 35 | 36 | this.bar.setName(title); 37 | this.bar.setPercent(percent); 38 | } 39 | 40 | public void addPlayer(ServerPlayerEntity player) { 41 | this.bar.addPlayer(player); 42 | } 43 | 44 | @Override 45 | public void close() { 46 | this.bar.setVisible(false); 47 | this.bar.clearPlayers(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/jaskarth/chunkpregen/ChunkPregen.java: -------------------------------------------------------------------------------- 1 | package com.jaskarth.chunkpregen; 2 | 3 | import net.fabricmc.api.ModInitializer; 4 | import net.minecraft.util.math.ChunkPos; 5 | import net.minecraft.util.math.ChunkSectionPos; 6 | import net.minecraft.world.chunk.Chunk; 7 | import net.minecraft.world.poi.PointOfInterestSet; 8 | import net.minecraft.world.poi.PointOfInterestStorage; 9 | import org.apache.logging.log4j.LogManager; 10 | import org.apache.logging.log4j.Logger; 11 | import com.jaskarth.chunkpregen.mixin.SerializingRegionBasedStorageAccessor; 12 | 13 | public final class ChunkPregen implements ModInitializer { 14 | public static final Logger LOGGER = LogManager.getLogger(ChunkPregen.class); 15 | 16 | @Override 17 | public void onInitialize() { 18 | Commands.init(); 19 | } 20 | 21 | /** 22 | * The goal here is to fix the POI memory leak that happens due to 23 | * {@link net.minecraft.world.storage.SerializingRegionBasedStorage#loadedElements} field never 24 | * actually removing POIs long after they become irrelevant. We do it here in chunk unloading 25 | * so that chunk that are fully unloaded now gets the POI removed from the POI cached storage map. 26 | */ 27 | public static void onChunkUnload(PointOfInterestStorage pointOfInterestStorage, Chunk chunk) { 28 | if (chunk == null) { 29 | return; 30 | } 31 | 32 | ChunkPos chunkPos = chunk.getPos(); 33 | pointOfInterestStorage.saveChunk(chunkPos); // Make sure all POI in chunk are saved to disk first. 34 | 35 | // Remove the cached POIs for this chunk's location. 36 | int SectionPosMinY = ChunkSectionPos.getSectionCoord(chunk.getBottomY()); 37 | for (int currentSectionY = 0; currentSectionY < chunk.countVerticalSections(); currentSectionY++) { 38 | long sectionPosKey = ChunkSectionPos.asLong(chunkPos.x, SectionPosMinY + currentSectionY, chunkPos.z); 39 | ((SerializingRegionBasedStorageAccessor)pointOfInterestStorage).getLoadedElements().remove(sectionPosKey); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/jaskarth/chunkpregen/iterator/OnionIterator.java: -------------------------------------------------------------------------------- 1 | package com.jaskarth.chunkpregen.iterator; 2 | 3 | import net.minecraft.util.math.ChunkPos; 4 | 5 | import java.util.Iterator; 6 | import java.util.NoSuchElementException; 7 | 8 | public final class OnionIterator implements Iterator { 9 | private static final byte EAST = 0; 10 | private static final byte SOUTH = 1; 11 | private static final byte WEST = 2; 12 | private static final byte NORTH = 3; 13 | private static final byte STOP = 4; 14 | 15 | private final int radius; 16 | 17 | private int x; 18 | private int z; 19 | 20 | private int distance = 0; 21 | private byte state = EAST; 22 | 23 | public OnionIterator(int radius) { 24 | this.radius = radius; 25 | } 26 | 27 | @Override 28 | public ChunkPos next() { 29 | if (!this.hasNext()) { 30 | throw new NoSuchElementException(); 31 | } 32 | 33 | ChunkPos pos = new ChunkPos(this.x, this.z); 34 | 35 | switch (this.state) { 36 | case EAST: 37 | if (++this.x >= this.distance) { 38 | this.state = SOUTH; 39 | if (this.distance > this.radius) { 40 | this.state = STOP; 41 | } 42 | } 43 | break; 44 | case SOUTH: 45 | if (++this.z >= this.distance) { 46 | this.state = WEST; 47 | } 48 | break; 49 | case WEST: 50 | if (--this.x <= -this.distance) { 51 | this.state = NORTH; 52 | } 53 | break; 54 | case NORTH: 55 | if (--this.z <= -this.distance) { 56 | this.state = EAST; 57 | this.distance++; 58 | } 59 | break; 60 | } 61 | 62 | if (this.distance == 0) { 63 | this.distance++; 64 | } 65 | 66 | return pos; 67 | } 68 | 69 | @Override 70 | public boolean hasNext() { 71 | return this.state != STOP; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/jaskarth/chunkpregen/iterator/CoarseOnionIterator.java: -------------------------------------------------------------------------------- 1 | package com.jaskarth.chunkpregen.iterator; 2 | 3 | import com.google.common.collect.AbstractIterator; 4 | import net.minecraft.util.math.ChunkPos; 5 | 6 | import java.util.Iterator; 7 | import java.util.NoSuchElementException; 8 | 9 | public final class CoarseOnionIterator extends AbstractIterator { 10 | private final int radius; 11 | private final int cellSize; 12 | 13 | private final OnionIterator cells; 14 | private CellIterator cell; 15 | 16 | public CoarseOnionIterator(int radius, int cellSize) { 17 | this.radius = radius; 18 | this.cellSize = cellSize; 19 | 20 | this.cells = new OnionIterator((radius + cellSize - 1) / cellSize); 21 | } 22 | 23 | @Override 24 | protected ChunkPos computeNext() { 25 | OnionIterator cells = this.cells; 26 | CellIterator cell = this.cell; 27 | while (cell == null || !cell.hasNext()) { 28 | if (!cells.hasNext()) { 29 | return this.endOfData(); 30 | } 31 | 32 | ChunkPos cellPos = cells.next(); 33 | this.cell = cell = this.createCellIterator(cellPos); 34 | } 35 | 36 | return cell.next(); 37 | } 38 | 39 | private CellIterator createCellIterator(ChunkPos pos) { 40 | int size = this.cellSize; 41 | int radius = this.radius; 42 | 43 | int x0 = pos.x * size; 44 | int z0 = pos.z * size; 45 | int x1 = x0 + size - 1; 46 | int z1 = z0 + size - 1; 47 | return new CellIterator( 48 | Math.max(x0, -radius), Math.max(z0, -radius), 49 | Math.min(x1, radius), Math.min(z1, radius) 50 | ); 51 | } 52 | 53 | private static final class CellIterator implements Iterator { 54 | private final int x0; 55 | private final int x1; 56 | private final int z1; 57 | 58 | private int x; 59 | private int z; 60 | 61 | private CellIterator(int x0, int z0, int x1, int z1) { 62 | this.x = x0; 63 | this.z = z0; 64 | this.x0 = x0; 65 | this.x1 = x1; 66 | this.z1 = z1; 67 | } 68 | 69 | @Override 70 | public boolean hasNext() { 71 | return this.x <= this.x1 && this.z <= this.z1; 72 | } 73 | 74 | @Override 75 | public ChunkPos next() { 76 | if (!this.hasNext()) { 77 | throw new NoSuchElementException(); 78 | } 79 | 80 | ChunkPos pos = new ChunkPos(this.x, this.z); 81 | if (this.x++ >= this.x1) { 82 | this.x = this.x0; 83 | this.z++; 84 | } 85 | 86 | return pos; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /src/main/java/com/jaskarth/chunkpregen/Commands.java: -------------------------------------------------------------------------------- 1 | package com.jaskarth.chunkpregen; 2 | 3 | import com.mojang.brigadier.Command; 4 | import com.mojang.brigadier.arguments.IntegerArgumentType; 5 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 6 | import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; 7 | import net.minecraft.server.command.CommandManager; 8 | import net.minecraft.server.command.ServerCommandSource; 9 | import net.minecraft.server.network.ServerPlayerEntity; 10 | import net.minecraft.text.Text; 11 | import net.minecraft.util.math.BlockPos; 12 | import net.minecraft.util.math.ChunkPos; 13 | 14 | import java.text.DecimalFormat; 15 | import java.util.concurrent.atomic.AtomicLong; 16 | 17 | public final class Commands { 18 | private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("#.00"); 19 | private static PregenerationTask activeTask; 20 | private static PregenBar pregenBar; 21 | 22 | public static void init() { 23 | CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { 24 | LiteralArgumentBuilder lab = CommandManager.literal("pregen") 25 | .requires(executor -> { 26 | // Remove requirement for cheats in singleplayer 27 | if (executor.getServer().isSingleplayer()) { 28 | return true; 29 | } 30 | 31 | // Needs to be opped in singplayer 32 | return executor.hasPermissionLevel(2); 33 | }); 34 | 35 | AtomicLong atotal = new AtomicLong(); 36 | AtomicLong atime = new AtomicLong(); 37 | 38 | // lab.then(CommandManager.literal("debug").executes(cmd -> { 39 | // for (int x = -100; x <= 100; x++) { 40 | // for (int z = -100; z <= 100; z++) { 41 | // long start = System.nanoTime(); 42 | // cmd.getSource().getWorld().getChunk(x, z); 43 | // long diff = System.nanoTime() - start; 44 | // 45 | // atotal.addAndGet(1); 46 | // atime.addAndGet(diff); 47 | // 48 | // System.out.println("Average: " + ((atime.get() / ((double) atotal.get())) / (1000.0) / 1000.0)); 49 | // } 50 | // } 51 | // 52 | // return 1; 53 | // })); 54 | 55 | 56 | lab.then(CommandManager.literal("start") 57 | .then(CommandManager.argument("radius", IntegerArgumentType.integer(0)) 58 | .executes(cmd -> { 59 | 60 | ServerCommandSource source = cmd.getSource(); 61 | if (activeTask != null) { 62 | source.sendFeedback(() -> Text.literal("Pregen already running. Please execute '/pregen stop' to start another pregeneration."), true); 63 | return Command.SINGLE_SUCCESS; 64 | } 65 | 66 | int radius = IntegerArgumentType.getInteger(cmd, "radius"); 67 | 68 | ChunkPos origin; 69 | if (source.getEntity() == null) { 70 | origin = new ChunkPos(0, 0); 71 | } else { 72 | origin = new ChunkPos(BlockPos.ofFloored(source.getPlayer().getPos())); 73 | } 74 | 75 | activeTask = new PregenerationTask(source.getWorld(), origin.x, origin.z, radius); 76 | int diameter = radius * 2 + 1; 77 | pregenBar = new PregenBar(); 78 | 79 | if (source.getEntity() instanceof ServerPlayerEntity) { 80 | pregenBar.addPlayer(source.getPlayer()); 81 | } 82 | 83 | source.sendFeedback(() -> Text.literal("Pregenerating " + activeTask.getTotalCount() + " chunks, in an area of " + diameter + "x" + diameter 84 | + " chunks (" + (diameter * 16) + "x" + (diameter * 16) + " blocks)."), true); 85 | 86 | activeTask.run(createPregenListener(source)); 87 | 88 | return Command.SINGLE_SUCCESS; 89 | }))); 90 | 91 | lab.then(CommandManager.literal("stop") 92 | .executes(cmd -> { 93 | if (activeTask != null) { 94 | activeTask.stop(); 95 | 96 | int count = activeTask.getOkCount() + activeTask.getErrorCount() + activeTask.getSkippedCount(); 97 | int total = activeTask.getTotalCount(); 98 | 99 | double percent = (double) count / total * 100.0; 100 | String message = "Pregen stopped! " + count + " out of " + total + " chunks generated. (" + percent + "%)"; 101 | cmd.getSource().sendFeedback(() -> Text.literal(message), true); 102 | 103 | pregenBar.close(); 104 | pregenBar = null; 105 | activeTask = null; 106 | } 107 | return Command.SINGLE_SUCCESS; 108 | })); 109 | 110 | lab.then(CommandManager.literal("status") 111 | .executes(cmd -> { 112 | if (activeTask != null) { 113 | int count = activeTask.getOkCount() + activeTask.getErrorCount() + activeTask.getSkippedCount(); 114 | int total = activeTask.getTotalCount(); 115 | 116 | double percent = (double) count / total * 100.0; 117 | String message = "Pregen status: " + count + " out of " + total + " chunks generated. (" + percent + "%)"; 118 | cmd.getSource().sendFeedback(() -> Text.literal(message), true); 119 | } else { 120 | cmd.getSource().sendFeedback(() -> Text.literal("No pregeneration currently running. Run /pregen start to start."), false); 121 | } 122 | return Command.SINGLE_SUCCESS; 123 | })); 124 | 125 | lab.then(CommandManager.literal("help"). 126 | executes(cmd -> { 127 | ServerCommandSource source = cmd.getSource(); 128 | 129 | source.sendFeedback(() -> Text.literal("§2/pregen start - Pregenerate a square centered on the player that is * 2 chunks long and wide."), false); 130 | source.sendFeedback(() -> Text.literal("§2/pregen stop - Stop pregeneration and displays the amount completed."), false); 131 | source.sendFeedback(() -> Text.literal("§2/pregen status - Display the amount of chunks pregenerated."), false); 132 | source.sendFeedback(() -> Text.literal("§2/pregen help - Display this message."), false); 133 | source.sendFeedback(() -> Text.literal("General tips: When running from a player, the center position will be the chunk you're standing on."), false); 134 | source.sendFeedback(() -> Text.literal("If running from a server console, the center position will be 0, 0. You can run pregen for different dimensions using /execute in pregen start "), false); 135 | return 1; 136 | })); 137 | 138 | dispatcher.register(lab); 139 | }); 140 | } 141 | 142 | private static PregenerationTask.Listener createPregenListener(ServerCommandSource source) { 143 | return new PregenerationTask.Listener() { 144 | @Override 145 | public void update(int ok, int error, int skipped, int total) { 146 | pregenBar.update(ok, error, skipped, total); 147 | } 148 | 149 | @Override 150 | public void complete(int error) { 151 | source.sendFeedback(() -> Text.literal("Pregeneration Done!"), true); 152 | 153 | if (error > 0) { 154 | source.sendFeedback(() -> Text.literal("Pregeneration experienced " + error + " errors! Check the log for more information"), true); 155 | } 156 | 157 | pregenBar.close(); 158 | pregenBar = null; 159 | activeTask = null; 160 | } 161 | }; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /src/main/java/com/jaskarth/chunkpregen/PregenerationTask.java: -------------------------------------------------------------------------------- 1 | package com.jaskarth.chunkpregen; 2 | 3 | import com.jaskarth.chunkpregen.iterator.CoarseOnionIterator; 4 | import com.jaskarth.chunkpregen.mixin.ThreadedAnvilChunkStorageAccessor; 5 | import com.mojang.datafixers.util.Either; 6 | import it.unimi.dsi.fastutil.longs.LongArrayList; 7 | import it.unimi.dsi.fastutil.longs.LongList; 8 | import net.minecraft.nbt.NbtCompound; 9 | import net.minecraft.nbt.NbtString; 10 | import net.minecraft.nbt.scanner.NbtScanQuery; 11 | import net.minecraft.nbt.scanner.SelectiveNbtCollector; 12 | import net.minecraft.server.MinecraftServer; 13 | import net.minecraft.server.world.*; 14 | import net.minecraft.util.Util; 15 | import net.minecraft.util.math.ChunkPos; 16 | import net.minecraft.world.chunk.Chunk; 17 | import net.minecraft.world.chunk.ChunkStatus; 18 | import net.minecraft.world.chunk.WorldChunk; 19 | 20 | import java.util.Comparator; 21 | import java.util.Iterator; 22 | import java.util.concurrent.CompletableFuture; 23 | import java.util.concurrent.atomic.AtomicInteger; 24 | 25 | public final class PregenerationTask { 26 | private static final int BATCH_SIZE = 32; 27 | private static final int QUEUE_THRESHOLD = 8; 28 | private static final int COARSE_CELL_SIZE = 4; 29 | 30 | private final MinecraftServer server; 31 | private final ServerChunkManager chunkManager; 32 | private final ServerWorld serverLevel; 33 | 34 | private final Iterator iterator; 35 | private final int x; 36 | private final int z; 37 | private final int radius; 38 | 39 | private final int totalCount; 40 | 41 | private final Object queueLock = new Object(); 42 | 43 | private final AtomicInteger queuedCount = new AtomicInteger(); 44 | private final AtomicInteger okCount = new AtomicInteger(); 45 | private final AtomicInteger errorCount = new AtomicInteger(); 46 | private final AtomicInteger skippedCount = new AtomicInteger(); 47 | 48 | private volatile Listener listener; 49 | private volatile boolean stopped; 50 | 51 | public static final ChunkTicketType FABRIC_PREGEN_FORCED = ChunkTicketType.create("fabric_pregen_forced", Comparator.comparingLong(ChunkPos::toLong)); 52 | 53 | public PregenerationTask(ServerWorld world, int x, int z, int radius) { 54 | this.server = world.getServer(); 55 | this.chunkManager = world.getChunkManager(); 56 | this.serverLevel = world; 57 | 58 | this.iterator = new CoarseOnionIterator(radius, COARSE_CELL_SIZE); 59 | this.x = x; 60 | this.z = z; 61 | this.radius = radius; 62 | 63 | int diameter = radius * 2 + 1; 64 | this.totalCount = diameter * diameter; 65 | } 66 | 67 | public int getOkCount() { 68 | return this.okCount.get(); 69 | } 70 | 71 | public int getErrorCount() { 72 | return this.errorCount.get(); 73 | } 74 | 75 | public int getSkippedCount() { 76 | return this.skippedCount.get(); 77 | } 78 | 79 | public int getTotalCount() { 80 | return this.totalCount; 81 | } 82 | 83 | public void run(Listener listener) { 84 | if (this.listener != null) { 85 | throw new IllegalStateException("already running!"); 86 | } 87 | 88 | this.listener = listener; 89 | 90 | // Off thread chunk scanning to skip already generated chunks 91 | CompletableFuture.runAsync(this::tryEnqueueTasks, Util.getMainWorkerExecutor()); 92 | } 93 | 94 | public void stop() { 95 | synchronized (this.queueLock) { 96 | this.stopped = true; 97 | this.listener = null; 98 | } 99 | } 100 | 101 | private void tryEnqueueTasks() { 102 | synchronized (this.queueLock) { 103 | if (this.stopped) { 104 | return; 105 | } 106 | 107 | int enqueueCount = BATCH_SIZE - this.queuedCount.get(); 108 | if (enqueueCount <= 0) { 109 | return; 110 | } 111 | 112 | LongList chunks = this.collectChunks(enqueueCount); 113 | if (chunks.isEmpty()) { 114 | this.listener.complete(this.errorCount.get()); 115 | this.stopped = true; 116 | return; 117 | } 118 | 119 | this.queuedCount.getAndAdd(chunks.size()); 120 | 121 | // Keep on server thread as chunk acquiring and releasing (tickets) is not thread safe. 122 | this.server.submit(() -> this.enqueueChunks(chunks)); 123 | } 124 | } 125 | 126 | private void enqueueChunks(LongList chunks) { 127 | for (int i = 0; i < chunks.size(); i++) { 128 | long chunk = chunks.getLong(i); 129 | this.acquireChunk(chunk); 130 | } 131 | 132 | // tick the chunk manager to force the ChunkHolders to be created 133 | this.chunkManager.tick(() -> true, true); 134 | 135 | ServerChunkLoadingManager tacs = this.chunkManager.chunkLoadingManager; 136 | 137 | for (int i = 0; i < chunks.size(); i++) { 138 | long chunk = chunks.getLong(i); 139 | 140 | ChunkHolder holder = tacs.getChunkHolder(chunk); 141 | if (holder == null) { 142 | ChunkPregen.LOGGER.warn("Added ticket for chunk but it was not added! ({}; {})", ChunkPos.getPackedX(chunk), ChunkPos.getPackedZ(chunk)); 143 | this.acceptChunkResult(chunk, ChunkHolder.UNLOADED_WORLD_CHUNK); 144 | continue; 145 | } 146 | 147 | holder.getAccessibleFuture() 148 | 149 | // holder.getChunkAt(ChunkStatus.FULL, tacs) 150 | .whenCompleteAsync((result, throwable) -> { 151 | if (throwable == null) { 152 | this.acceptChunkResult(chunk, result); 153 | } else { 154 | ChunkPregen.LOGGER.warn("Encountered unexpected error while generating chunk", throwable); 155 | this.acceptChunkResult(chunk, ChunkHolder.UNLOADED_WORLD_CHUNK); 156 | } 157 | }, runnable -> ((ThreadedAnvilChunkStorageAccessor)tacs).getMainExecutor().send(ChunkTaskPrioritySystem.createMessage(holder, runnable))); 158 | } 159 | } 160 | 161 | private void acceptChunkResult(long chunk, OptionalChunk result) { 162 | this.server.submit(() -> this.releaseChunk(chunk)); 163 | 164 | if (result.isPresent()) { 165 | this.okCount.getAndIncrement(); 166 | } else { 167 | this.errorCount.getAndIncrement(); 168 | } 169 | 170 | this.listener.update(this.okCount.get(), this.errorCount.get(), this.skippedCount.get(), this.totalCount); 171 | 172 | int queuedCount = this.queuedCount.decrementAndGet(); 173 | if (queuedCount <= QUEUE_THRESHOLD) { 174 | this.tryEnqueueTasks(); 175 | } 176 | } 177 | 178 | private LongList collectChunks(int count) { 179 | LongList chunks = new LongArrayList(count); 180 | 181 | Iterator iterator = this.iterator; 182 | for (int i = 0; i < count && iterator.hasNext();) { 183 | ChunkPos chunkPosInLocalSpace = iterator.next(); 184 | if (isChunkFullyGenerated(chunkPosInLocalSpace)) { 185 | this.skippedCount.incrementAndGet(); 186 | this.listener.update(this.okCount.get(), this.errorCount.get(), this.skippedCount.get(), this.totalCount); 187 | continue; 188 | } 189 | 190 | chunks.add(ChunkPos.toLong(chunkPosInLocalSpace.x + this.x, chunkPosInLocalSpace.z + this.z)); 191 | i++; 192 | } 193 | 194 | return chunks; 195 | } 196 | 197 | private void acquireChunk(long chunk) { 198 | ChunkPos pos = new ChunkPos(chunk); 199 | this.chunkManager.addTicket(FABRIC_PREGEN_FORCED, pos, 0, pos); 200 | } 201 | 202 | private void releaseChunk(long chunk) { 203 | ChunkPos pos = new ChunkPos(chunk); 204 | this.chunkManager.removeTicket(FABRIC_PREGEN_FORCED, pos, 0, pos); 205 | } 206 | 207 | private boolean isChunkFullyGenerated(ChunkPos chunkPosInLocalSpace) { 208 | ChunkPos chunkPosInWorldSpace = new ChunkPos(chunkPosInLocalSpace.x + this.x, chunkPosInLocalSpace.z + this.z); 209 | SelectiveNbtCollector nbtCollector = new SelectiveNbtCollector(new NbtScanQuery(NbtString.TYPE, "Status")); 210 | this.chunkManager.getChunkIoWorker().scanChunk(chunkPosInWorldSpace, nbtCollector).join(); 211 | 212 | if (nbtCollector.getRoot() instanceof NbtCompound nbtCompound) { 213 | return nbtCompound.getString("Status").equals("minecraft:full"); 214 | } 215 | 216 | return false; 217 | } 218 | 219 | public interface Listener { 220 | void update(int ok, int error, int skipped, int total); 221 | 222 | void complete(int error); 223 | } 224 | } 225 | --------------------------------------------------------------------------------