diff --git a/_Misc Files/setup documentation/Gitlab Docker Container Setup.txt b/_Misc Files/setup documentation/Gitlab Docker Container Setup.txt index 2ec91c357..6c7af33e7 100644 --- a/_Misc Files/setup documentation/Gitlab Docker Container Setup.txt +++ b/_Misc Files/setup documentation/Gitlab Docker Container Setup.txt @@ -32,6 +32,11 @@ gitlab project -> settings (on the left menu bar) -> CI/CD -> Runners and you should see the newly registered runner (the description should show up under the runner's auto generated ID) +5. optional config +Open Docker Desktop -> Containers -> Files, go to the file "etc/gitlab-runner/config.toml" +concurrent can be changed to allow for concurrent builds. + + Additional commands: diff --git a/api/src/main/java/com/seibel/distanthorizons/api/enums/worldGeneration/EDhApiWorldGeneratorReturnType.java b/api/src/main/java/com/seibel/distanthorizons/api/enums/worldGeneration/EDhApiWorldGeneratorReturnType.java new file mode 100644 index 000000000..30e411b69 --- /dev/null +++ b/api/src/main/java/com/seibel/distanthorizons/api/enums/worldGeneration/EDhApiWorldGeneratorReturnType.java @@ -0,0 +1,56 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.api.enums.worldGeneration; + +import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode; +import com.seibel.distanthorizons.api.interfaces.override.worldGenerator.IDhApiWorldGenerator; + +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; + +/** + * VANILLA_CHUNKS,
+ * API_CHUNKS
+ * + * @author Builderb0y, James Seibel + * @version 2023-12-21 + * @since API 1.1.0 + */ +public enum EDhApiWorldGeneratorReturnType +{ + /** + * when this constant is returned by {@link IDhApiWorldGenerator#getReturnType()}, + * {@link IDhApiWorldGenerator#generateChunks(int, int, byte, byte, EDhApiDistantGeneratorMode, ExecutorService, Consumer)} + * will be used when generating terrain. + * + * @since API 1.1.0 + */ + VANILLA_CHUNKS, + + /** + * when this constant is returned by {@link IDhApiWorldGenerator#getReturnType()}, + * {@link IDhApiWorldGenerator#generateApiChunks(int, int, byte, byte, EDhApiDistantGeneratorMode, ExecutorService, Consumer)} + * will be used when generating terrain. + * + * @since API 1.1.0 + */ + API_CHUNKS; + +} diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java index 5ec1293f0..16f3c0a68 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java @@ -19,9 +19,11 @@ package com.seibel.distanthorizons.api.interfaces.override.worldGenerator; +import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGeneratorReturnType; import com.seibel.distanthorizons.api.interfaces.override.IDhApiOverrideable; import com.seibel.distanthorizons.api.enums.EDhApiDetailLevel; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode; +import com.seibel.distanthorizons.api.objects.data.DhApiChunk; import java.io.Closeable; import java.util.concurrent.CompletableFuture; @@ -102,7 +104,8 @@ public interface IDhApiWorldGenerator extends Closeable, IDhApiOverrideable //=================// /** - * This method is called by Distant Horizons to generate terrain over a given area.

+ * This method is called by Distant Horizons to generate terrain over a given area when + * {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#VANILLA_CHUNKS}.

* * After a chunk has been generated it (and any necessary supporting objects as listed below) should be passed into the * resultConsumer's {@link Consumer#accept} method. If the Consumer is given the wrong data @@ -115,22 +118,80 @@ public interface IDhApiWorldGenerator extends Closeable, IDhApiOverrideable * - [net.minecraft.world.level.chunk.ChunkAccess]
* - [net.minecraft.world.level.ServerLevel] or [net.minecraft.world.level.ClientLevel]
* - * @param chunkPosMinX the chunk X position closest to negative infinity - * @param chunkPosMinZ the chunk Z position closest to negative infinity + * @implNote the default implementation of this method throws an {@link UnsupportedOperationException}, + * and must be overridden when {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#VANILLA_CHUNKS}. + * since {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#VANILLA_CHUNKS} by default, + * this method must also be overridden when {@link #getReturnType()} is NOT overridden. + * + * @param chunkPosMinX the chunk X position closest to negative infinity + * @param chunkPosMinZ the chunk Z position closest to negative infinity * @param granularity TODO find a central location to store the definition of granularity. For now it is stored in the Core method: WorldGenerationQueue#startGenerationEvent * @param targetDataDetail the LOD Detail level requested to generate. See {@link EDhApiDetailLevel} for additional information. * @param generatorMode how far into the world gen pipeline this method run. See {@link EDhApiDistantGeneratorMode} for additional documentation. * @param worldGeneratorThreadPool the thread pool that should be used when generating the returned {@link CompletableFuture}. * @param resultConsumer the consumer that should be fired whenever a chunk finishes generating. - * + * * @return a future that should run on the worldGeneratorThreadPool and complete once the given generation task has completed. - * + * * @since API 1.0.0 */ - CompletableFuture generateChunks( - int chunkPosMinX, int chunkPosMinZ, - byte granularity, byte targetDataDetail, EDhApiDistantGeneratorMode generatorMode, - ExecutorService worldGeneratorThreadPool, Consumer resultConsumer); + default CompletableFuture generateChunks( + int chunkPosMinX, + int chunkPosMinZ, + byte granularity, + byte targetDataDetail, + EDhApiDistantGeneratorMode generatorMode, + ExecutorService worldGeneratorThreadPool, + Consumer resultConsumer + ) + { + throw new UnsupportedOperationException(); + } + + /** + * This method is called by Distant Horizons to generate terrain over a given area when + * {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}.

+ * + * After the {@link DhApiChunk} has been generated, it should be passed into the + * resultConsumer's {@link Consumer#accept(Object)} method. + * + * @implNote the default implementation of this method throws an {@link UnsupportedOperationException}, + * and must be overridden when {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}. + * + * @param chunkPosMinX the chunk X position closest to negative infinity + * @param chunkPosMinZ the chunk Z position closest to negative infinity + * @param granularity TODO find a central location to store the definition of granularity. For now it is stored in the Core method: WorldGenerationQueue#startGenerationEvent + * @param targetDataDetail the LOD Detail level requested to generate. See {@link EDhApiDetailLevel} for additional information. + * @param generatorMode how far into the world gen pipeline this method run. See {@link EDhApiDistantGeneratorMode} for additional documentation. + * @param worldGeneratorThreadPool the thread pool that should be used when generating the returned {@link CompletableFuture}. + * @param resultConsumer the consumer that should be fired whenever a chunk finishes generating. + * + * @return a future that should run on the worldGeneratorThreadPool and complete once the given generation task has completed. + * + * @since API 1.1.0 + */ + default CompletableFuture generateApiChunks( + int chunkPosMinX, + int chunkPosMinZ, + byte granularity, + byte targetDataDetail, + EDhApiDistantGeneratorMode generatorMode, + ExecutorService worldGeneratorThreadPool, + Consumer resultConsumer + ) + { + throw new UnsupportedOperationException(); + } + + /** + * This method controls how Distant Horizons requests generated chunks. + * By default, the return value is {@link EDhApiWorldGeneratorReturnType#VANILLA_CHUNKS}, + * which means that {@link #generateChunks(int, int, byte, byte, EDhApiDistantGeneratorMode, ExecutorService, Consumer)} + * will be invoked whenever Distant Horizons wants to generate terrain with this world generator. + * + * @since API 1.1.0 + */ + default EDhApiWorldGeneratorReturnType getReturnType() { return EDhApiWorldGeneratorReturnType.VANILLA_CHUNKS; } diff --git a/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiChunk.java b/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiChunk.java new file mode 100644 index 000000000..b8f479ff7 --- /dev/null +++ b/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiChunk.java @@ -0,0 +1,136 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.api.objects.data; + +import com.seibel.distanthorizons.api.interfaces.factories.IDhApiWrapperFactory; +import com.seibel.distanthorizons.api.interfaces.override.worldGenerator.IDhApiWorldGenerator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Contains a list of {@link DhApiTerrainDataPoint} representing the blocks in a Minecraft chunk. + * + * @author Builderb0y, James Seibel + * @version 2023-12-21 + * @since API 1.1.0 + * + * @see IDhApiWrapperFactory + * @see DhApiTerrainDataPoint + * @see IDhApiWorldGenerator + */ +public class DhApiChunk +{ + public final int chunkPosX; + public final int chunkPosZ; + + public final int topYBlockPos; + public final int bottomYBlockPos; + + private final List> dataPoints; + + + + //==============// + // constructors // + //==============// + + public DhApiChunk(int chunkPosX, int chunkPosZ, int topYBlockPos, int bottomYBlockPos) + { + this.chunkPosX = chunkPosX; + this.chunkPosZ = chunkPosZ; + this.topYBlockPos = topYBlockPos; + this.bottomYBlockPos = bottomYBlockPos; + + // populate the array to prevent null pointers + this.dataPoints = new ArrayList<>(16 * 16); // 256 + for (int i = 0; i < (16*16); i++) + { + this.dataPoints.add(i, null); + } + } + + + + //=================// + // getters/setters // + //=================// + + /** + * @param relX a block position between 0 and 15 (inclusive) representing the X axis in the chunk + * @param relZ a block position between 0 and 15 (inclusive) representing the Z axis in the chunk + * @return the {@link DhApiTerrainDataPoint}'s representing the blocks at the relative X and Z position in the chunk. + * @throws IndexOutOfBoundsException if relX or relZ are outside the chunk + */ + public List getDataPoints(int relX, int relZ) throws IndexOutOfBoundsException + { + throwIfRelativePosOutOfBounds(relX, relZ); + return this.dataPoints.get((relZ << 4) | relX); + } + + /** + * @param relX a block position between 0 and 15 (inclusive) representing the X axis in the chunk + * @param relZ a block position between 0 and 15 (inclusive) representing the Z axis in the chunk + * @param dataPoints Represents the blocks at the relative X and Z position in the chunk. + * Cannot contain null objects or data points with any detail level but 0 (block-sized). + * @throws IndexOutOfBoundsException if relX or relZ are outside the chunk + */ + public void setDataPoints(int relX, int relZ, List dataPoints) throws IndexOutOfBoundsException, IllegalArgumentException + { + throwIfRelativePosOutOfBounds(relX, relZ); + + // validate the incoming datapoints + if (dataPoints != null) + { + for (int i = 0; i < dataPoints.size(); i++) // standard for-loop used instead of an enhanced for-loop to slightly reduce GC overhead due to iterator allocation + { + DhApiTerrainDataPoint dataPoint = dataPoints.get(i); + if (dataPoint == null) + { + throw new IllegalArgumentException("Null DhApiTerrainDataPoints are not allowed. If you want to represent empty terrain, please use AIR."); + } + + if (dataPoint.detailLevel != 0) + { + throw new IllegalArgumentException("DhApiTerrainDataPoints has the wrong detail level ["+dataPoint.detailLevel+"], all data points must be block sized; IE their detail level must be [0]."); + } + } + } + + this.dataPoints.set((relZ << 4) | relX, dataPoints); + } + + + + //================// + // helper methods // + //================// + + /** Included to prevent users accidentally setting columns outside the chunk */ + private static void throwIfRelativePosOutOfBounds(int relX, int relZ) + { + if (relX < 0 || relX > 15 || + relZ < 0 || relZ > 15) + { + throw new IndexOutOfBoundsException("Relative block positions must be between 0 and 15 (inclusive) the block pos: ("+relX+","+relZ+") is outside of those boundaries."); + } + } + +} \ No newline at end of file diff --git a/api/src/main/resources/fabric.mod.json b/api/src/main/resources/not.fabric.mod.json similarity index 100% rename from api/src/main/resources/fabric.mod.json rename to api/src/main/resources/not.fabric.mod.json diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java index 1a1030dde..8ac100f70 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java @@ -19,9 +19,14 @@ package com.seibel.distanthorizons.core.dataObjects.transformers; +import java.util.List; + +import com.seibel.distanthorizons.api.objects.data.DhApiChunk; +import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint; import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; @@ -39,6 +44,11 @@ public class LodDataBuilder private static boolean getTopErrorLogged = false; + + //============// + // converters // + //============// + public static ChunkSizedFullDataAccessor createChunkData(IChunkWrapper chunkWrapper) { if (!canGenerateLodFromChunk(chunkWrapper)) @@ -121,10 +131,54 @@ public class LodDataBuilder return chunkData; } - public static boolean canGenerateLodFromChunk(IChunkWrapper chunk) + /** @throws ClassCastException if an API user returns the wrong object type(s) */ + public static ChunkSizedFullDataAccessor createApiChunkData(DhApiChunk dataPoints) throws ClassCastException { - //return true; - return chunk != null && chunk.isLightCorrect(); // TODO client only chunks return chunks with bad lighting, preventing chunk building (or transparent only chunks) + ChunkSizedFullDataAccessor accessor = new ChunkSizedFullDataAccessor(new DhChunkPos(dataPoints.chunkPosX, dataPoints.chunkPosZ)); + for (int relZ = 0; relZ < LodUtil.CHUNK_WIDTH; relZ++) + { + for (int relX = 0; relX < LodUtil.CHUNK_WIDTH; relX++) + { + List columnDataPoints = dataPoints.getDataPoints(relX, relZ); + + + // this null check does 2 nice things at the same time: + // if columnDataPoints is null, + // then packedDataPoints will be of length 0 + // AND the below loop won't run. + int size = (columnDataPoints != null) ? columnDataPoints.size() : 0; + + long[] packedDataPoints = new long[size]; + for (int index = 0; index < size; index++) + { + DhApiTerrainDataPoint dataPoint = columnDataPoints.get(index); + + int id = accessor.getMapping().addIfNotPresentAndGetId( + (IBiomeWrapper) (dataPoint.biomeWrapper), + (IBlockStateWrapper) (dataPoint.blockStateWrapper) + ); + + packedDataPoints[index] = FullDataPointUtil.encode( + id, + dataPoint.topYBlockPos - dataPoint.bottomYBlockPos, + dataPoint.bottomYBlockPos - dataPoints.topYBlockPos, + (byte) (dataPoint.lightLevel) + ); + } + + accessor.setSingleColumn(packedDataPoints, relX, relZ); + } + } + + return accessor; } + + + + //================// + // helper methods // + //================// + + public static boolean canGenerateLodFromChunk(IChunkWrapper chunk) { return chunk != null && chunk.isLightCorrect(); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java index e0d99e001..00fd702f0 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java @@ -21,7 +21,8 @@ package com.seibel.distanthorizons.core.generation; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode; import com.seibel.distanthorizons.api.interfaces.override.worldGenerator.IDhApiWorldGenerator; -import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener; +import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGeneratorReturnType; +import com.seibel.distanthorizons.api.objects.data.DhApiChunk; import com.seibel.distanthorizons.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.generation.tasks.*; @@ -33,8 +34,8 @@ import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dataObjects.transformers.LodDataBuilder; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; +import com.seibel.distanthorizons.core.util.LodUtil.AssertFailureException; import com.seibel.distanthorizons.core.util.ThreadUtil; -import com.seibel.distanthorizons.core.util.threading.RateLimitedThreadPoolExecutor; import com.seibel.distanthorizons.core.util.objects.UncheckedInterruptedException; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPools; @@ -50,6 +51,7 @@ import java.util.function.Consumer; public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRenderable { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); private final IDhApiWorldGenerator generator; @@ -408,26 +410,72 @@ public class WorldGenerationQueue implements IWorldGenerationQueue, IDebugRender * (So, with a generator whose only gen data detail is 0, it is the same as a MC chunk.) */ private CompletableFuture startGenerationEvent( - DhChunkPos chunkPosMin, - byte granularity, byte targetDataDetail, - Consumer chunkDataConsumer) + DhChunkPos chunkPosMin, + byte granularity, + byte targetDataDetail, + Consumer chunkDataConsumer + ) { EDhApiDistantGeneratorMode generatorMode = Config.Client.Advanced.WorldGenerator.distantGeneratorMode.get(); - return this.generator.generateChunks(chunkPosMin.x, chunkPosMin.z, granularity, targetDataDetail, generatorMode, ThreadPools.getWorldGenExecutor(), (generatedObjectArray) -> + EDhApiWorldGeneratorReturnType returnType = this.generator.getReturnType(); + switch (returnType) { - try + case VANILLA_CHUNKS: { - IChunkWrapper chunk = SingletonInjector.INSTANCE.get(IWrapperFactory.class).createChunkWrapper(generatedObjectArray); - ChunkSizedFullDataAccessor chunkDataAccessor = LodDataBuilder.createChunkData(chunk); - LodUtil.assertTrue(chunkDataAccessor != null); - chunkDataConsumer.accept(chunkDataAccessor); + return this.generator.generateChunks( + chunkPosMin.x, + chunkPosMin.z, + granularity, + targetDataDetail, + generatorMode, + ThreadPools.getWorldGenExecutor(), + (Object[] generatedObjectArray) -> + { + try + { + IChunkWrapper chunk = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray); + ChunkSizedFullDataAccessor chunkDataAccessor = LodDataBuilder.createChunkData(chunk); + LodUtil.assertTrue(chunkDataAccessor != null); + chunkDataConsumer.accept(chunkDataAccessor); + } + catch (ClassCastException e) + { + LOGGER.error("World generator return type incorrect. Error: [" + e.getMessage() + "]. World generator disabled.", e); + Config.Client.Advanced.WorldGenerator.enableDistantGeneration.set(false); + } + } + ); } - catch (ClassCastException e) + case API_CHUNKS: + { + return this.generator.generateApiChunks( + chunkPosMin.x, + chunkPosMin.z, + granularity, + targetDataDetail, + generatorMode, + ThreadPools.getWorldGenExecutor(), + (DhApiChunk dataPoints) -> + { + try + { + ChunkSizedFullDataAccessor chunkDataAccessor = LodDataBuilder.createApiChunkData(dataPoints); + chunkDataConsumer.accept(chunkDataAccessor); + } + catch (ClassCastException e) + { + LOGGER.error("World generator return type incorrect. Error: [" + e.getMessage() + "]. World generator disabled.", e); + Config.Client.Advanced.WorldGenerator.enableDistantGeneration.set(false); + } + } + ); + } + default: { - DhLoggerBuilder.getLogger().error("World generator return type incorrect. Error: [" + e.getMessage() + "]. World generator disabled.", e); Config.Client.Advanced.WorldGenerator.enableDistantGeneration.set(false); + throw new AssertFailureException("Unknown return type: " + returnType); } - }); + } }