From 08ede3351db40c0c0185f93699277d9f81d15ebd Mon Sep 17 00:00:00 2001 From: James Seibel Date: Thu, 2 Oct 2025 18:03:27 -0500 Subject: [PATCH] Add DhApiChunkProcessingEvent --- .../DhApiChunkProcessingEvent.java | 197 ++++++++++++++++++ .../events/interfaces/IDhApiEventParam.java | 18 ++ .../DependencyInjection/ApiEventInjector.java | 18 +- .../fullData/sources/FullDataSourceV2.java | 3 +- .../transformers/LodDataBuilder.java | 78 +++++-- .../core/generation/WorldGenerationQueue.java | 2 +- .../core/level/AbstractDhLevel.java | 2 +- 7 files changed, 287 insertions(+), 31 deletions(-) create mode 100644 api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiChunkProcessingEvent.java diff --git a/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiChunkProcessingEvent.java b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiChunkProcessingEvent.java new file mode 100644 index 000000000..4995aa57f --- /dev/null +++ b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/abstractEvents/DhApiChunkProcessingEvent.java @@ -0,0 +1,197 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020 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.methods.events.abstractEvents; + +import com.seibel.distanthorizons.api.interfaces.block.IDhApiBiomeWrapper; +import com.seibel.distanthorizons.api.interfaces.block.IDhApiBlockStateWrapper; +import com.seibel.distanthorizons.api.interfaces.factories.IDhApiWrapperFactory; +import com.seibel.distanthorizons.api.interfaces.world.IDhApiLevelWrapper; +import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent; +import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam; +import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam; + +/** + * Used to override which blocks may be stored in a given chunk. + * This can be used for X-ray prevention or to replace problematic mod blocks + * that don't fit into the {@link IDhApiBlockStateWrapper} format DH requires + * (IE modded blocks that use NBT data + * to determine their model and/or texture).

+ * + * This event is fired for each block or biome change when DH is processing a chunk. + * A change happens when DH finds a different block or biome while walking through a chunk. + * For example with the block sequence:
+ * stone -> stone -> air -> stone
+ * This event would be fired for the first, third, and forth blocks in the sequence + * (IE the first stone, first air, and last stone respectively).

+ * + * The order DH will process blocks is undefined so a specific ordering shouldn't be relied upon for your logic to function.

+ * + * Threading note: this event may be called concurrently across multiple threads.
+ * Performance note: this event will be called very frequently, avoid expensive lookups or other slow operations if possible.
+ * + * @see DhApiLevelLoadEvent + * @see IDhApiWrapperFactory + * + * @author James Seibel + * @version 2025-09-29 + * @since API 4.1.0 + */ +public abstract class DhApiChunkProcessingEvent implements IDhApiEvent +{ + public abstract void blockOrBiomeChangedDuringChunkProcessing(DhApiEventParam event); + + + //=========================// + // internal DH API methods // + //=========================// + + @Override + public final void fireEvent(DhApiEventParam event) { this.blockOrBiomeChangedDuringChunkProcessing(event); } + + + //==================// + // parameter object // + //==================// + + public static class EventParam implements IDhApiEventParam + { + /** The saved level. */ + public final IDhApiLevelWrapper levelWrapper; + + /** the processed chunk's X pos in chunk coordinates */ + public final int chunkX; + /** the processed chunk's Z pos in chunk coordinates */ + public final int chunkZ; + + + public int relativeBlockPosX; + public int blockPosY; + public int relativeBlockPosZ; + + public IDhApiBlockStateWrapper currentBlock; + public IDhApiBiomeWrapper currentBiome; + + private IDhApiBlockStateWrapper newBlock; + private IDhApiBiomeWrapper newBiome; + + + + //=============// + // constructor // + //=============// + + public EventParam(IDhApiLevelWrapper newLevelWrapper, int chunkX, int chunkZ) + { + this.levelWrapper = newLevelWrapper; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + } + + /** + * Internal method use by Distant Horizons + * to set up this event. + */ + public void updateForPosition( + int relativeBlockPosX, int blockPosY, int relativeBlockPosZ, + IDhApiBlockStateWrapper currentBlock, + IDhApiBiomeWrapper currentBiome) + { + this.relativeBlockPosX = relativeBlockPosX; + this.blockPosY = blockPosY; + this.relativeBlockPosZ = relativeBlockPosZ; + + this.newBlock = null; + this.newBiome = null; + + this.currentBlock = currentBlock; + this.currentBiome = currentBiome; + } + + + + //=================// + // getters/setters // + //=================// + + /** + * Sets the {@link IDhApiBlockStateWrapper} that should be used at this event's current position in the chunk. + * If you don't want to modify the block at this event's current position, + * either don't call this method or pass in null.
+ * Passing in null will remove the override, meaning the original block will be used.

+ * + * A {@link IDhApiWrapperFactory} should be used to get the {@link IDhApiBlockStateWrapper} that's returned. + * Attempting to create your own {@link IDhApiBlockStateWrapper} will cause a {@link ClassCastException}.

+ * + * If multiple API users are listening to this event the override may already have been set. + * With that in mind it is recommended to check if an override has already been set via + * {@link EventParam#getBlockOverride()} to handle that occurrence.
+ * Note that the order of API events firing is undefined so a specific order shouldn't be relied upon.

+ * + * @see IDhApiWrapperFactory + */ + public void setBlockOverride(IDhApiBlockStateWrapper block) { this.newBlock = block; } + /** + * Returns the currently overriding block for this position. + * This will be null if no other API event has set the override. + */ + public IDhApiBlockStateWrapper getBlockOverride() { return this.newBlock; } + + + + /** + * Sets the {@link IDhApiBiomeWrapper} that should be used at this event's current position in the chunk. + * If you don't want to modify the biome at this event's current position, + * either don't call this method or pass in null.
+ * Passing in null will remove the override, meaning the original biome will be used.

+ * + * A {@link IDhApiWrapperFactory} should be used to get the {@link IDhApiBiomeWrapper} that's returned. + * Attempting to create your own {@link IDhApiBiomeWrapper} will cause a {@link ClassCastException}.

+ * + * If multiple API users are listening to this event the override may already have been set. + * With that in mind it is recommended to check if an override has already been set via + * {@link EventParam#getBiomeOverride()} ()} to handle that occurrence.
+ * Note that the order of API events firing is undefined so a specific order shouldn't be relied upon.

+ * + * @see IDhApiWrapperFactory + */ + public void setBiomeOverride(IDhApiBiomeWrapper biome) { this.newBiome = biome; } + /** + * Returns the currently overriding biome for this position. + * This will be null if no other API event has set the override. + */ + public IDhApiBiomeWrapper getBiomeOverride() { return this.newBiome; } + + + + /** + * Returns the same instance of this event. + * Copying this event isn't recommended due to + * how often it would be called per chunk, creating + * unnecessary garbage collector pressure. + */ + @Override + public EventParam copy() { return this; } + + @Override + public boolean getCopyBeforeFire() { return false; } + + } + +} \ No newline at end of file diff --git a/api/src/main/java/com/seibel/distanthorizons/api/methods/events/interfaces/IDhApiEventParam.java b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/interfaces/IDhApiEventParam.java index daa3d3446..07fa5af89 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/methods/events/interfaces/IDhApiEventParam.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/methods/events/interfaces/IDhApiEventParam.java @@ -9,5 +9,23 @@ import com.seibel.distanthorizons.api.interfaces.util.IDhApiCopyable; */ public interface IDhApiEventParam extends IDhApiCopyable { + /** + * Internal DH use.

+ * + * Most API events will clone their parameters + * before firing to prevent API implementors + * from modifying the properties causing + * any subsequent listeners to see the wrong data.

+ * + * However, this can be overridden for API events that shouldn't + * be cloned before firing. + * Generally that would be done for performance reasons + * where an event may fire hundreds or thousands of times + * in quick succession or where the event parameter is needed + * internally by DH after firing. + * + * @since API 4.1.0 + */ + default boolean getCopyBeforeFire() { return true; } } diff --git a/api/src/main/java/com/seibel/distanthorizons/coreapi/DependencyInjection/ApiEventInjector.java b/api/src/main/java/com/seibel/distanthorizons/coreapi/DependencyInjection/ApiEventInjector.java index e36d65cb0..4d02bde84 100644 --- a/api/src/main/java/com/seibel/distanthorizons/coreapi/DependencyInjection/ApiEventInjector.java +++ b/api/src/main/java/com/seibel/distanthorizons/coreapi/DependencyInjection/ApiEventInjector.java @@ -148,14 +148,18 @@ public class ApiEventInjector extends DependencyInjector implements T input = eventInput; if (eventInput instanceof IDhApiEventParam) { - try + IDhApiEventParam dhApiEventParam = (IDhApiEventParam) eventInput; + if (dhApiEventParam.getCopyBeforeFire()) { - //noinspection unchecked - input = (T) ((IDhApiEventParam) eventInput).copy(); - } - catch (Exception e) - { - LOGGER.error("Unable to clone event parameter ["+eventInput.getClass().getSimpleName()+"], error: ["+e.getMessage()+"].", e); + try + { + //noinspection unchecked + input = (T) dhApiEventParam.copy(); + } + catch (Exception e) + { + LOGGER.error("Unable to clone event parameter [" + eventInput.getClass().getSimpleName() + "], error: [" + e.getMessage() + "].", e); + } } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV2.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV2.java index 6407d150f..00516cdb9 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV2.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/FullDataSourceV2.java @@ -35,6 +35,7 @@ import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.util.*; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.coreapi.ModInfo; import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList; @@ -132,7 +133,7 @@ public class FullDataSourceV2 // constructors // //==============// - public static FullDataSourceV2 createFromChunk(IChunkWrapper chunkWrapper) { return LodDataBuilder.createFromChunk(chunkWrapper); } + public static FullDataSourceV2 createFromChunk(ILevelWrapper levelWrapper, IChunkWrapper chunkWrapper) { return LodDataBuilder.createFromChunk(levelWrapper, chunkWrapper); } public static FullDataSourceV2 createFromLegacyDataSourceV1(FullDataSourceV1 legacyData) { 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 185c13326..7fe5fd52f 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 @@ -24,15 +24,15 @@ import java.util.List; import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep; +import com.seibel.distanthorizons.api.interfaces.block.IDhApiBiomeWrapper; +import com.seibel.distanthorizons.api.interfaces.block.IDhApiBlockStateWrapper; +import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkProcessingEvent; import com.seibel.distanthorizons.api.objects.data.DhApiChunk; import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; -import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; -import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; -import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPosMutable; import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.LodUtil; @@ -43,6 +43,8 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IMutableBlockPosWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import it.unimi.dsi.fastutil.longs.LongArrayList; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; @@ -50,7 +52,8 @@ import org.jetbrains.annotations.Nullable; public class LodDataBuilder { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - private static final IBlockStateWrapper AIR = SingletonInjector.INSTANCE.get(IWrapperFactory.class).getAirBlockStateWrapper(); + private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); + private static final IBlockStateWrapper AIR = WRAPPER_FACTORY.getAirBlockStateWrapper(); private static boolean getTopErrorLogged = false; @@ -60,11 +63,14 @@ public class LodDataBuilder // converters // //============// - public static FullDataSourceV2 createFromChunk(IChunkWrapper chunkWrapper) + public static FullDataSourceV2 createFromChunk(ILevelWrapper levelWrapper, IChunkWrapper chunkWrapper) { // only block lighting is needed here, sky lighting is populated at the data source stage LodUtil.assertTrue(chunkWrapper.isDhBlockLightingCorrect()); + int chunkPosX = chunkWrapper.getChunkPos().getX(); + int chunkPosZ = chunkWrapper.getChunkPos().getZ(); + int sectionPosX = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getX()); int sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getZ()); long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); @@ -119,6 +125,9 @@ public class LodDataBuilder IMutableBlockPosWrapper mcBlockPos = chunkWrapper.getMutableBlockPosWrapper(); IBlockStateWrapper previousBlockState = null; + DhApiChunkProcessingEvent.EventParam mutableChunkProcessedEventParam + = new DhApiChunkProcessingEvent.EventParam(levelWrapper, chunkPosX, chunkPosZ); + int minBuildHeight = chunkWrapper.getMinNonEmptyHeight(); int exclusiveMaxBuildHeight = chunkWrapper.getExclusiveMaxBuildHeight(); int inclusiveMinBuildHeight = chunkWrapper.getInclusiveMinBuildHeight(); @@ -144,9 +153,9 @@ public class LodDataBuilder } int lastY = exclusiveMaxBuildHeight; - IBiomeWrapper biome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ); - IBlockStateWrapper blockState = AIR; - int mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState); + IBiomeWrapper currentBiome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ); + IBlockStateWrapper currentBlockState = AIR; + int mappedId = dataSource.mapping.addIfNotPresentAndGetId(currentBiome, currentBlockState); // Determine lighting (we are at the height limit. There are no torches here, and sky is not obscured.) // TODO: Per face lighting someday? byte blockLight = LodUtil.MIN_MC_LIGHT; @@ -162,7 +171,8 @@ public class LodDataBuilder // Go up until we reach open air or the world limit IBlockStateWrapper topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState); - while (!topBlockState.isAir() && y < exclusiveMaxBuildHeight) + while (!topBlockState.isAir() + && y < exclusiveMaxBuildHeight) { try { @@ -194,22 +204,46 @@ public class LodDataBuilder byte newSkyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, y + 1, relBlockZ); // Save the biome/block change if different from previous - if (!newBiome.equals(biome) || !newBlockState.equals(blockState) || forceSingleBlock) + if (!newBiome.equals(currentBiome) + || !newBlockState.equals(currentBlockState) + || forceSingleBlock) { // if the previous block potentially colors this block // make this block a single entry, aka add the next block even if it is the same // this is done to allow fire, snow, flowers, etc. to properly color the top of columns vs the whole column forceSingleBlock = - !blockState.isAir() - && !blockState.isSolid() - && !blockState.isLiquid() - && blockState.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE; + !currentBlockState.isAir() + && !currentBlockState.isSolid() + && !currentBlockState.isLiquid() + && currentBlockState.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE; + + + // check for API overrides + { + mutableChunkProcessedEventParam.updateForPosition(relBlockX, y, relBlockZ, newBlockState, newBiome); + ApiEventInjector.INSTANCE.fireAllEvents(DhApiChunkProcessingEvent.class, mutableChunkProcessedEventParam); + + // did the API user override this block? + if (mutableChunkProcessedEventParam.getBlockOverride() != null) + { + // API users shouldn't be creating their own IBlockStateWrapper objects + newBlockState = (IBlockStateWrapper)mutableChunkProcessedEventParam.getBlockOverride(); + } + + // did the API user override this biome? + if (mutableChunkProcessedEventParam.getBiomeOverride() != null) + { + // API users shouldn't be creating their own IBlockStateWrapper objects + newBiome = (IBiomeWrapper) mutableChunkProcessedEventParam.getBiomeOverride(); + } + } + longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - inclusiveMinBuildHeight, blockLight, skyLight)); - biome = newBiome; - blockState = newBlockState; + currentBiome = newBiome; + currentBlockState = newBlockState; - mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState); + mappedId = dataSource.mapping.addIfNotPresentAndGetId(currentBiome, currentBlockState); blockLight = newBlockLight; skyLight = newSkyLight; lastY = y; @@ -544,11 +578,11 @@ public class LodDataBuilder - //================// - // helper methods // - //================// + //==================// + // internal helpers // + //==================// - public static int getXOrZSectionPosFromChunkPos(int chunkXOrZPos) + private static int getXOrZSectionPosFromChunkPos(int chunkXOrZPos) { // get the section position int sectionPos = chunkXOrZPos; @@ -557,4 +591,6 @@ public class LodDataBuilder return sectionPos; } + + } 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 d435e0ba5..893ec01c6 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 @@ -411,7 +411,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb try { IChunkWrapper chunk = WRAPPER_FACTORY.createChunkWrapper(generatedObjectArray); - try (FullDataSourceV2 dataSource = LodDataBuilder.createFromChunk(chunk)) + try (FullDataSourceV2 dataSource = LodDataBuilder.createFromChunk(this.level.getLevelWrapper(), chunk)) { LodUtil.assertTrue(dataSource != null); dataSourceConsumer.accept(dataSource); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhLevel.java b/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhLevel.java index 9b92da70a..d979bf2a1 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhLevel.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/AbstractDhLevel.java @@ -156,7 +156,7 @@ public abstract class AbstractDhLevel implements IDhLevel @Override public void updateChunkAsync(IChunkWrapper chunkWrapper, int chunkHash) { - try (FullDataSourceV2 dataSource = FullDataSourceV2.createFromChunk(chunkWrapper)) + try (FullDataSourceV2 dataSource = FullDataSourceV2.createFromChunk(this.getLevelWrapper(), chunkWrapper)) { if (dataSource == null) {