Add DhApiChunkProcessingEvent

This commit is contained in:
James Seibel
2025-10-02 18:03:27 -05:00
parent 9690c898b0
commit 08ede3351d
7 changed files with 287 additions and 31 deletions
@@ -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 <https://www.gnu.org/licenses/>.
*/
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). <br/><br/>
*
* 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:<br/>
* <code> stone -> stone -> air -> stone </code> <br/>
* 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). <br/> <br/>
*
* The order DH will process blocks is undefined so a specific ordering shouldn't be relied upon for your logic to function. <br/> <br/>
*
* <b>Threading note:</b> this event may be called concurrently across multiple threads. <br/>
* <b>Performance note:</b> this event will be called very frequently, avoid expensive lookups or other slow operations if possible. <br/>
*
* @see DhApiLevelLoadEvent
* @see IDhApiWrapperFactory
*
* @author James Seibel
* @version 2025-09-29
* @since API 4.1.0
*/
public abstract class DhApiChunkProcessingEvent implements IDhApiEvent<DhApiChunkProcessingEvent.EventParam>
{
public abstract void blockOrBiomeChangedDuringChunkProcessing(DhApiEventParam<EventParam> event);
//=========================//
// internal DH API methods //
//=========================//
@Override
public final void fireEvent(DhApiEventParam<EventParam> 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. <br>
* Passing in null will remove the override, meaning the original block will be used. <br><br>
*
* 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}. <br/> <br/>
*
* 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. <br>
* Note that the order of API events firing is undefined so a specific order shouldn't be relied upon. <br><br>
*
* @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. <br>
* Passing in null will remove the override, meaning the original biome will be used. <br><br>
*
* 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}. <br/> <br/>
*
* 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. <br>
* Note that the order of API events firing is undefined so a specific order shouldn't be relied upon. <br><br>
*
* @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; }
}
}
@@ -9,5 +9,23 @@ import com.seibel.distanthorizons.api.interfaces.util.IDhApiCopyable;
*/
public interface IDhApiEventParam extends IDhApiCopyable
{
/**
* Internal DH use. <br> <br>
*
* 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. <br><br>
*
* 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; }
}
@@ -148,14 +148,18 @@ public class ApiEventInjector extends DependencyInjector<IDhApiEvent> 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);
}
}
}
@@ -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)
{
@@ -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;
}
}
@@ -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);
@@ -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)
{