package dev.redstudio.alfheim.lighting; import dev.redstudio.alfheim.utils.DeduplicatedLongQueue; import dev.redstudio.redcore.math.ClampUtil; import net.minecraft.block.state.IBlockState; import net.minecraft.util.BlockPos; import net.minecraft.util.EnumFacing; import net.minecraft.util.Vec3i; import net.minecraft.world.EnumSkyBlock; import net.minecraft.world.World; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.storage.ExtendedBlockStorage; /** * Modified by lax1dude not to abuse interfaces * * @author Luna Lage (Desoroxxx) * @author kappa-maintainer * @author embeddedt * @author Angeline (@jellysquid) * @since 1.0 */ public final class LightingEngine { private static final byte MAX_LIGHT_LEVEL = 15; private final World world; // Layout of longs: [padding(4)] [y(8)] [x(26)] [z(26)] private final DeduplicatedLongQueue[] lightUpdateQueues = new DeduplicatedLongQueue[EnumSkyBlock.values().length]; // Layout of longs: see above private final DeduplicatedLongQueue[] darkeningQueues = new DeduplicatedLongQueue[MAX_LIGHT_LEVEL + 1]; private final DeduplicatedLongQueue[] brighteningQueues = new DeduplicatedLongQueue[MAX_LIGHT_LEVEL + 1]; // Layout of longs: [newLight(4)] [pos(60)] private final DeduplicatedLongQueue initialBrightenings; // Layout of longs: [padding(4)] [pos(60)] private final DeduplicatedLongQueue initialDarkenings; private boolean updating = false; // Layout parameters // Length of bit segments private static final int L_X = 26, L_Y = 8, L_Z = 26, L_L = 4; // Bit segment shifts/positions private static final int S_Z = 0, S_X = S_Z + L_Z, S_Y = S_X + L_X, S_L = S_Y + L_Y; // Bit segment masks private static final long M_X = (1L << L_X) - 1, M_Y = (1L << L_Y) - 1, M_Z = (1L << L_Z) - 1, M_L = (1L << L_L) - 1, M_POS = (M_Y << S_Y) | (M_X << S_X) | (M_Z << S_Z); // Bit to check whether y had overflow private static final long Y_CHECK = 1L << (S_Y + L_Y); private static final long[] neighborShifts = new long[6]; static { for (byte i = 0; i < 6; ++i) { final Vec3i offset = EnumFacing._VALUES[i].getDirectionVec(); neighborShifts[i] = ((long) offset.getY() << S_Y) | ((long) offset.getX() << S_X) | ((long) offset.getZ() << S_Z); } } // Mask to extract chunk identifier private static final long M_CHUNK = ((M_X >> 4) << (4 + S_X)) | ((M_Z >> 4) << (4 + S_Z)); // Iteration state data // Cache position to avoid allocation of new object each time private final BlockPos currentPos = new BlockPos(); private Chunk currentChunk; private long currentChunkIdentifier; private long currentData; // Cached data about neighboring blocks (of tempPos) private boolean isNeighborDataValid = false; private final NeighborInfo[] neighborInfos = new NeighborInfo[6]; private DeduplicatedLongQueue currentQueue; public LightingEngine(final World world) { this.world = world; initialBrightenings = new DeduplicatedLongQueue(16384); initialDarkenings = new DeduplicatedLongQueue(16384); for (int i = 0; i < EnumSkyBlock.values().length; ++i) lightUpdateQueues[i] = new DeduplicatedLongQueue(16384); for (int i = 0; i < darkeningQueues.length; ++i) darkeningQueues[i] = new DeduplicatedLongQueue(16384); for (int i = 0; i < brighteningQueues.length; ++i) brighteningQueues[i] = new DeduplicatedLongQueue(16384); for (int i = 0; i < neighborInfos.length; ++i) neighborInfos[i] = new NeighborInfo(); } /** * Schedules a light update for the specified light type and position to be * processed later by * {@link LightingEngine#processLightUpdatesForType(EnumSkyBlock)} */ public void scheduleLightUpdate(final EnumSkyBlock lightType, final BlockPos pos) { scheduleLightUpdate(lightType, encodeWorldCoord(pos)); } /** * Schedules a light update for the specified light type and position to be * processed later by {@link LightingEngine#processLightUpdates()} */ private void scheduleLightUpdate(final EnumSkyBlock lightType, final long blockPos) { lightUpdateQueues[lightType.ordinal()].enqueue(blockPos); } /** * Calls {@link LightingEngine#processLightUpdatesForType(EnumSkyBlock)} for * both light types */ public void processLightUpdates() { processLightUpdatesForType(EnumSkyBlock.SKY); processLightUpdatesForType(EnumSkyBlock.BLOCK); } /** * Processes light updates of the given light type */ public void processLightUpdatesForType(final EnumSkyBlock lightType) { final DeduplicatedLongQueue queue = lightUpdateQueues[lightType.ordinal()]; // Quickly check if the queue is empty before we acquire a more expensive lock. if (queue.isEmpty()) return; processLightUpdatesForTypeInner(lightType, queue); } private void processLightUpdatesForTypeInner(final EnumSkyBlock lightType, final DeduplicatedLongQueue queue) { // Avoid nested calls if (updating) throw new IllegalStateException("Already processing updates!"); updating = true; currentChunkIdentifier = -1; // Reset chunk cache currentQueue = queue; if (currentQueue != null) currentQueue.newDeduplicationSet(); // Process the queued updates and enqueue them for further processing while (nextItem()) { if (currentChunk == null) continue; final byte oldLight = getCursorCachedLight(lightType); final byte newLight = calculateNewLightFromCursor(lightType); if (oldLight < newLight) initialBrightenings.enqueue(((long) newLight << S_L) | currentData); // Don't enqueue directly for // brightening to avoid // duplicate scheduling else if (oldLight > newLight) initialDarkenings.enqueue(currentData); // Don't enqueue directly for darkening to avoid duplicate // scheduling } currentQueue = initialBrightenings; if (currentQueue != null) currentQueue.newDeduplicationSet(); while (nextItem()) { final byte newLight = (byte) (currentData >> S_L & M_L); if (newLight > getCursorCachedLight(lightType)) enqueueBrightening(currentPos, currentData & M_POS, newLight, currentChunk, lightType); // Sets the // light to // newLight to // only schedule // once. Clear // leading bits // of curData // for later } currentQueue = initialDarkenings; if (currentQueue != null) currentQueue.newDeduplicationSet(); while (nextItem()) { final byte oldLight = getCursorCachedLight(lightType); if (oldLight != 0) enqueueDarkening(currentPos, currentData, oldLight, currentChunk, lightType); // Sets the light to zero // to only schedule once } // Iterate through enqueued updates (brightening and darkening in parallel) from // brightest to darkest so that we only need to iterate once for (byte currentLight = MAX_LIGHT_LEVEL; currentLight >= 0; --currentLight) { currentQueue = darkeningQueues[currentLight]; if (currentQueue != null) currentQueue.newDeduplicationSet(); while (nextItem()) { // Don't darken if we got brighter due to some other change if (getCursorCachedLight(lightType) >= currentLight) continue; final IBlockState blockState = currentChunk.getBlockState(currentPos); final byte luminosity = getCursorLuminosity(blockState, lightType); final byte opacity; // If luminosity is high enough, opacity is irrelevant if (luminosity >= MAX_LIGHT_LEVEL - 1) opacity = 1; else opacity = getPosOpacity(currentPos, blockState); // Only darken neighbors if we indeed became darker if (calculateNewLightFromCursor(luminosity, opacity, lightType) < currentLight) { // Need to calculate new light value from neighbors IGNORING neighbors which are // scheduled for darkening byte newLight = luminosity; fetchNeighborDataFromCursor(lightType); for (final NeighborInfo neighborInfo : neighborInfos) { final Chunk neighborChunk = neighborInfo.chunk; if (neighborChunk == null) continue; final byte neighborLight = neighborInfo.light; if (neighborLight == 0) continue; final BlockPos neighborPos = neighborInfo.mutableBlockPos; if (currentLight - getPosOpacity(neighborPos, neighborChunk .getBlockState(neighborPos)) >= neighborLight) /* * Schedule neighbor for darkening if we * possibly light it */ { enqueueDarkening(neighborPos, neighborInfo.key, neighborLight, neighborChunk, lightType); } else /* Only use for new light calculation if not */ { // If we can't darken the neighbor, no one else can (because of processing // order) -> safe to let us be illuminated by it newLight = (byte) Math.max(newLight, neighborLight - opacity); } } // Schedule brightening since light level was set to 0 enqueueBrighteningFromCursor(newLight, lightType); } else /* * We didn't become darker, so we need to re-set our initial light value (was * set to zero) and notify neighbors */ { enqueueBrighteningFromCursor(currentLight, lightType); // Do not spread to neighbors immediately to // avoid scheduling multiple times } } currentQueue = brighteningQueues[currentLight]; if (currentQueue != null) currentQueue.newDeduplicationSet(); while (nextItem()) { final byte oldLight = getCursorCachedLight(lightType); // Only process this if nothing else has happened at this position since // scheduling if (oldLight == currentLight) { world.notifyLightSet(currentPos); if (currentLight > 1) spreadLightFromCursor(currentLight, lightType); } } } updating = false; } /** * Gets data for neighbors of {@link #currentPos} and saves the results into * neighbor state data members. If a neighbor can't be accessed/doesn't exist, * the corresponding entry in neighborChunks is null - others are not reset */ private void fetchNeighborDataFromCursor(final EnumSkyBlock lightType) { // Only update if curPos was changed if (isNeighborDataValid) return; isNeighborDataValid = true; for (int i = 0; i < neighborInfos.length; ++i) { final NeighborInfo neighborInfo = neighborInfos[i]; final long neighborLongPos = neighborInfo.key = currentData + neighborShifts[i]; if ((neighborLongPos & Y_CHECK) != 0) { neighborInfo.chunk = null; continue; } final BlockPos neighborPos = decodeWorldCoord(neighborInfo.mutableBlockPos, neighborLongPos); final Chunk neighborChunk; if ((neighborLongPos & M_CHUNK) == currentChunkIdentifier) neighborChunk = neighborInfo.chunk = currentChunk; else neighborChunk = neighborInfo.chunk = getChunk(neighborPos); if (neighborChunk != null) { final ExtendedBlockStorage neighborSection = neighborChunk .getBlockStorageArray()[neighborPos.getY() >> 4]; neighborInfo.light = getCachedLightFor(neighborChunk, neighborSection, neighborPos, lightType); } } } private static byte getCachedLightFor(final Chunk chunk, final ExtendedBlockStorage storage, final BlockPos blockPos, final EnumSkyBlock type) { final int x = blockPos.getX() & 15; final int y = blockPos.getY(); final int z = blockPos.getZ() & 15; if (storage == null) return type == EnumSkyBlock.SKY && chunk.canSeeSky(blockPos) ? (byte) type.defaultLightValue : 0; else if (type == EnumSkyBlock.SKY) return chunk.getWorld().provider.getHasNoSky() ? 0 : (byte) storage.getExtSkylightValue(x, y & 15, z); else return type == EnumSkyBlock.BLOCK ? (byte) storage.getExtBlocklightValue(x, y & 15, z) : (byte) type.defaultLightValue; } private byte calculateNewLightFromCursor(final EnumSkyBlock lightType) { final IBlockState blockState = currentChunk.getBlockState(currentPos); final byte luminosity = getCursorLuminosity(blockState, lightType); final byte opacity; if (luminosity >= MAX_LIGHT_LEVEL - 1) opacity = 1; else opacity = getPosOpacity(currentPos, blockState); return calculateNewLightFromCursor(luminosity, opacity, lightType); } private byte calculateNewLightFromCursor(final byte luminosity, final byte opacity, final EnumSkyBlock lightType) { if (luminosity >= MAX_LIGHT_LEVEL - opacity) return luminosity; byte newLight = luminosity; fetchNeighborDataFromCursor(lightType); for (final NeighborInfo neighborInfo : neighborInfos) { if (neighborInfo.chunk == null) continue; newLight = (byte) Math.max(neighborInfo.light - opacity, newLight); } return newLight; } private void spreadLightFromCursor(final byte currentLight, final EnumSkyBlock lightType) { fetchNeighborDataFromCursor(lightType); for (final NeighborInfo neighborInfo : neighborInfos) { final Chunk neighborChunk = neighborInfo.chunk; if (neighborChunk == null || currentLight < neighborInfo.light) continue; final BlockPos neighborBlockPos = neighborInfo.mutableBlockPos; final byte newLight = (byte) (currentLight - getPosOpacity(neighborBlockPos, neighborChunk.getBlockState(neighborBlockPos))); if (newLight > neighborInfo.light) enqueueBrightening(neighborBlockPos, neighborInfo.key, newLight, neighborChunk, lightType); } } private void enqueueBrighteningFromCursor(final byte newLight, final EnumSkyBlock lightType) { enqueueBrightening(currentPos, currentData, newLight, currentChunk, lightType); } /** * Enqueues the blockPos for brightening and sets its light value to newLight */ private void enqueueBrightening(final BlockPos blockPos, final long longPos, final byte newLight, final Chunk chunk, final EnumSkyBlock lightType) { brighteningQueues[newLight].enqueue(longPos); chunk.setLightFor(lightType, blockPos, newLight); } /** * Enqueues the blockPos for darkening and sets its light value to 0 */ private void enqueueDarkening(final BlockPos blockPos, final long longPos, final byte oldLight, final Chunk chunk, final EnumSkyBlock lightType) { darkeningQueues[oldLight].enqueue(longPos); chunk.setLightFor(lightType, blockPos, 0); } private static BlockPos decodeWorldCoord(final BlockPos mutableBlockPos, final long longPos) { return mutableBlockPos.func_181079_c((int) (longPos >> S_X & M_X) - (1 << L_X - 1), (int) (longPos >> S_Y & M_Y), (int) (longPos >> S_Z & M_Z) - (1 << L_Z - 1)); } private static long encodeWorldCoord(final BlockPos pos) { return ((long) pos.getY() << S_Y) | ((long) pos.getX() + (1 << L_X - 1) << S_X) | ((long) pos.getZ() + (1 << L_Z - 1) << S_Z); } /** * Polls a new item from {@link #currentQueue} and fills in state data members * * @return If there was an item to poll */ private boolean nextItem() { if (currentQueue.isEmpty()) { currentQueue = null; return false; } currentData = currentQueue.dequeue(); isNeighborDataValid = false; decodeWorldCoord(currentPos, currentData); final long chunkIdentifier = currentData & M_CHUNK; if (currentChunkIdentifier != chunkIdentifier) { currentChunk = getChunk(currentPos); currentChunkIdentifier = chunkIdentifier; } return true; } private byte getCursorCachedLight(final EnumSkyBlock lightType) { return currentChunk.alfheim$getCachedLightFor(lightType, currentPos); } /** * Calculates the luminosity for {@link #currentPos}, taking into account the * light type */ private byte getCursorLuminosity(final IBlockState state, final EnumSkyBlock lightType) { if (lightType == EnumSkyBlock.SKY) { if (currentChunk.canSeeSky(currentPos)) return (byte) EnumSkyBlock.SKY.defaultLightValue; else return 0; } return (byte) ClampUtil.clampMinFirst(state.getBlock().getLightValue(), 0, MAX_LIGHT_LEVEL); } private byte getPosOpacity(final BlockPos blockPos, final IBlockState blockState) { return (byte) ClampUtil.clampMinFirst(blockState.getBlock().getLightOpacity(), 1, MAX_LIGHT_LEVEL); } private Chunk getChunk(final BlockPos blockPos) { return world.getChunkProvider().getLoadedChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4); } private static final class NeighborInfo { public final BlockPos mutableBlockPos = new BlockPos(); public Chunk chunk; public byte light; public long key; } }