Add DhApiChunkProcessingEvent
This commit is contained in:
+197
@@ -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; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+18
@@ -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; }
|
||||
|
||||
}
|
||||
|
||||
+11
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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)
|
||||
{
|
||||
|
||||
+57
-21
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user