/*
* This file is part of Cubic Chunks Mod, licensed under the MIT License (MIT).
*
* Copyright (c) 2015 contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package cubicchunks.lighting;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenCustomHashMap;
import it.unimi.dsi.fastutil.ints.IntHash;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.EnumSkyBlock;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import cubicchunks.util.FastCubeBlockAccess;
import cubicchunks.world.ICubeProvider;
import cubicchunks.world.ICubicWorld;
import cubicchunks.world.IHeightMap;
import cubicchunks.world.column.Column;
import cubicchunks.world.cube.Cube;
import static cubicchunks.util.Coords.blockToCube;
import static cubicchunks.util.Coords.blockToLocal;
import static cubicchunks.util.Coords.cubeToMaxBlock;
import static cubicchunks.util.Coords.cubeToMinBlock;
import static cubicchunks.util.Coords.getCubeCenter;
import static net.minecraft.util.math.BlockPos.MutableBlockPos;
/**
* Notes on world.checkLightFor(): Decreasing light value: Light is recalculated starting from 0 ONLY for blocks where
* rawLightValue is equal to savedLightValue (ie. updating skylight source that is not there anymore). Otherwise
* existing light values are assumed to be correct. Generates and updates cube initial lighting, and propagates light
* changes caused by generating cube downwards.
* <p>
* Used only when changes are caused by pre-populator terrain generation.
* <p>
* THIS SHOULD ONLY EVER BE USED ONCE PER CUBE.
*/
//TODO: make it also update blocklight
public class FirstLightProcessor {
private static final int LIGHT_UPDATE_RADIUS = 17;
private static final int CUBE_RADIUS = Cube.SIZE/2;
private static final int UPDATE_BUFFER_RADIUS = 1;
private static final int UPDATE_RADIUS = LIGHT_UPDATE_RADIUS + CUBE_RADIUS + UPDATE_BUFFER_RADIUS;
private static final IntHash.Strategy CUBE_Y_HASH = new IntHash.Strategy() {
@Override
public int hashCode(int e) {
return e;
}
@Override
public boolean equals(int a, int b) {
return a == b;
}
};
private final MutableBlockPos mutablePos = new MutableBlockPos();
private final ICubeProvider cache;
/**
* Creates a new FirstLightProcessor for the given world.
*
* @param world the world for which the FirstLightProcessor will be used
*/
public FirstLightProcessor(ICubicWorld world) {
this.cache = world.getCubeCache();
}
/**
* Initializes skylight in the given cube. The skylight will be consistent with respect to the world configuration
* and already existing cubes. It is however possible for cubes being considered lit at this stage to be occluded
* by cubes being generated further up.
*
* @param cube the cube whose skylight is to be initialized
*/
public void initializeSkylight(Cube cube) {
if (cube.getCubicWorld().getProvider().getHasNoSky()) {
return;
}
IHeightMap opacityIndex = cube.getColumn().getOpacityIndex();
int cubeMinY = cubeToMinBlock(cube.getY());
for (int localX = 0; localX < Cube.SIZE; ++localX) {
for (int localZ = 0; localZ < Cube.SIZE; ++localZ) {
for (int localY = Cube.SIZE - 1; localY >= 0; --localY) {
if (opacityIndex.isOccluded(localX, cubeMinY + localY, localZ)) {
break;
}
cube.setSkylight(localX, localY, localZ, 15);
}
}
}
}
/**
* Diffuses skylight in the given cube and all cubes affected by this update.
*
* @param cube the cube whose skylight is to be initialized
*/
public void diffuseSkylight(Cube cube) {
if (cube.getCubicWorld().getProvider().getHasNoSky()) {
cube.setInitialLightingDone(true);
return;
}
ICubicWorld world = cube.getCubicWorld();
// Cache min/max Y, generating them may be expensive
int[][] minBlockYArr = new int[16][16];
int[][] maxBlockYArr = new int[16][16];
int minBlockX = cubeToMinBlock(cube.getX());
int maxBlockX = cubeToMaxBlock(cube.getX());
int minBlockZ = cubeToMinBlock(cube.getZ());
int maxBlockZ = cubeToMaxBlock(cube.getZ());
// Determine the block columns that require updating. If there is nothing to update, store contradicting data so
// we can skip the column later.
for (int localX = 0; localX <= Cube.SIZE - 1; ++localX) {
for (int localZ = 0; localZ <= Cube.SIZE - 1; ++localZ) {
Pair<Integer, Integer> minMax = getMinMaxLightUpdateY(cube, localX, localZ);
minBlockYArr[localX][localZ] = minMax == null ? Integer.MAX_VALUE : minMax.getLeft();
maxBlockYArr[localX][localZ] = minMax == null ? Integer.MIN_VALUE : minMax.getRight();
}
}
Int2ObjectMap<FastCubeBlockAccess> blockAccessMap = new Int2ObjectOpenCustomHashMap<>(10, 0.75f, CUBE_Y_HASH);
Column column = cube.getColumn();
for (int blockX = minBlockX; blockX <= maxBlockX; blockX++) {
for (int blockZ = minBlockZ; blockZ <= maxBlockZ; blockZ++) {
this.mutablePos.setPos(blockX, this.mutablePos.getY(), blockZ);
int minBlockY = minBlockYArr[blockX - minBlockX][blockZ - minBlockZ];
int maxBlockY = maxBlockYArr[blockX - minBlockX][blockZ - minBlockZ];
// If no update is needed, skip the block column.
if (minBlockY > maxBlockY) {
continue;
}
int topBlockY = getOcclusionHeight(column, blockToLocal(blockX), blockToLocal(blockZ));
// Iterate over all affected cubes.
Iterable<Cube> cubes = column.getLoadedCubes(blockToCube(maxBlockY), blockToCube(minBlockY));
for (Cube otherCube : cubes) {
int cubeY = otherCube.getY();
if (otherCube != cube && canStopUpdating(cube, this.mutablePos, topBlockY)) {
break;
}
if (otherCube != cube && !cube.isInitialLightingDone()) {
continue;
}
// Skip this cube if an update is not possible.
if (!canUpdateCube(otherCube)) {
int minScheduledY = Math.max(cubeToMinBlock(cubeY), minBlockY);
int maxScheduledY = Math.min(cubeToMaxBlock(cubeY), maxBlockY);
// Queue the update to be processed once the cube is ready for it.
world.getLightingManager().queueDiffuseUpdate(otherCube, this.mutablePos.getX(), this.mutablePos.getZ(), minScheduledY, maxScheduledY);
continue;
}
// Update the block column in this cube.
if (!diffuseSkylightInBlockColumn(otherCube, this.mutablePos, minBlockY, maxBlockY, blockAccessMap)) {
throw new IllegalStateException("Check light failed at " + this.mutablePos + "!");
}
}
}
}
cube.setInitialLightingDone(true);
}
/**
* Diffuses skylight inside of the given cube in the block column specified by the given MutableBlockPos. The
* update is limited vertically by minBlockY and maxBlockY.
*
* @param cube the cube inside of which the skylight is to be diffused
* @param pos the xz-position of the block column to be updated
* @param minBlockY the lower bound of the section to be updated
* @param maxBlockY the upper bound of the section to be updated
*
* @return true if the update was successful, false otherwise
*/
private boolean diffuseSkylightInBlockColumn(Cube cube, MutableBlockPos pos, int minBlockY, int maxBlockY, Int2ObjectMap<FastCubeBlockAccess> blockAccessMap) {
ICubicWorld world = cube.getCubicWorld();
int cubeMinBlockY = cubeToMinBlock(cube.getY());
int cubeMaxBlockY = cubeToMaxBlock(cube.getY());
int maxBlockYInCube = Math.min(cubeMaxBlockY, maxBlockY);
int minBlockYInCube = Math.max(cubeMinBlockY, minBlockY);
FastCubeBlockAccess blockAccess = blockAccessMap.get(cube.getY());
if (blockAccess == null) {
blockAccess = new FastCubeBlockAccess(this.cache, cube, UPDATE_BUFFER_RADIUS);
blockAccessMap.put(cube.getY(), blockAccess);
}
for (int blockY = maxBlockYInCube; blockY >= minBlockYInCube; --blockY) {
pos.setY(blockY);
if (!needsSkylightUpdate(blockAccess, pos)) {
continue;
}
if (!world.checkLightFor(EnumSkyBlock.SKY, pos)) {
return false;
}
}
return true;
}
/**
* Determines if the block at the given position requires a skylight update.
*
* @param access a FastCubeBlockAccess providing access to the block
* @param pos the block's global position
*
* @return true if the specified block needs a skylight update, false otherwise
*/
private static boolean needsSkylightUpdate(FastCubeBlockAccess access, MutableBlockPos pos) {
// Opaque blocks don't need update. Nothing can emit skylight, and skylight can't get into them nor out of them.
if (access.getBlockLightOpacity(pos) >= 15) {
return false;
}
// This is the logic that world.checkLightFor uses to determine if it should continue updating.
// This is done here to avoid isAreaLoaded call (a lot of them quickly add up to a lot of time).
// It first calculates the expected skylight value of this block and then it checks the neighbors' saved values,
// if the saved value matches the expected value, it will be updated.
int computedLight = access.computeLightValue(pos);
for (EnumFacing facing : EnumFacing.values()) {
pos.move(facing);
int currentLight = access.getLightFor(EnumSkyBlock.SKY, pos);
int currentOpacity = Math.max(1, access.getBlockLightOpacity(pos));
pos.move(facing.getOpposite());
if (computedLight == currentLight - currentOpacity) {
return true;
}
}
return false;
}
/**
* Determines if light in the given cube can be updated.
*
* @param cube the cube whose light is supposed to be updated
*
* @return true if light in the given cube can be updated, false otherwise
*/
private static boolean canUpdateCube(Cube cube) {
BlockPos cubeCenter = getCubeCenter(cube);
return cube.getCubicWorld().testForCubes(cubeCenter, UPDATE_RADIUS, c -> c != null);
}
/**
* Determines if the block column of the given cube as specified by the given BlockPos has valid lighting and thus
* does not require further updating.
*
* @param cube the cube whose light is supposed to be updated
* @param pos the xz-position of the block column being updated
* @param topBlockY the y-coordinate of the highest block in the block column
*
* @return true if updating the skylight of the specified block column is no longer required, false otherwise
*/
private static boolean canStopUpdating(Cube cube, MutableBlockPos pos, int topBlockY) {
// Note: This logic does not apply to the main cube being updated, but only to those below it!
pos.setY(cube.getCoords().getMaxBlockY());
boolean isDirectSkylight = pos.getY() > topBlockY;
int lightValue = cube.getLightFor(EnumSkyBlock.SKY, pos);
// If the cube does not receive direct skylight and the light value does not need updating, then all blocks
// further down do not need to be updated either.
return !isDirectSkylight && lightValue < 15;
}
/**
* Returns the y-coordinate of the highest occluding block in the specified block column. If there exists no such
* block {@link #DEFAULT_OCCLUSION_HEIGHT} will be returned instead.
*
* @param column the column containing the block column
* @param localX the block column's local x-coordinate
* @param localZ the block column's local z-coordinate
*
* @return the y-coordinate of the highest occluding block in the specified block column or {@link
* #DEFAULT_OCCLUSION_HEIGHT} if no such block exists
*/
private static int getOcclusionHeight(Column column, int localX, int localZ) {
return column.getOpacityIndex().getTopBlockY(localX, localZ);
}
/**
* Returns the y-coordinate of the highest occluding block in the specified block column, that is underneath the
* cube at the given y-coordinate. If there exists no such block {@link #DEFAULT_OCCLUSION_HEIGHT} will be returned
* instead.
*
* @param column the column containing the block column
* @param blockX the block column's global x-coordinate
* @param blockZ the block column's global z-coordinate
* @param cubeY the y-coordinate of the cube underneath which the highest occluding block is to be found
*
* @return the y-coordinate of the highest occluding block underneath the given cube in the specified block column
* or {@link #DEFAULT_OCCLUSION_HEIGHT} if no such block exists
*/
private static int getOcclusionHeightBelowCubeY(Column column, int blockX, int blockZ, int cubeY) {
IHeightMap index = column.getOpacityIndex();
return index.getTopBlockYBelow(blockToLocal(blockX), blockToLocal(blockZ), cubeToMinBlock(cubeY));
}
/**
* Determines which vertical section of the specified block column in the given cube requires a lighting update
* based on the current occlusion in the cube's column.
*
* @param cube the cube inside of which the skylight is to be updated
* @param localX the local x-coordinate of the block column
* @param localZ the local z-coordinate of the block column
*
* @return a pair containing the minimum and the maximum y-coordinate to be updated in the given cube
*/
private static ImmutablePair<Integer, Integer> getMinMaxLightUpdateY(Cube cube, int localX, int localZ) {
Column column = cube.getColumn();
int heightMax = getOcclusionHeight(column, localX, localZ);//==Y of the top block
// If the given cube is above the highest occluding block in the column, everything is fully lit.
int cubeY = cube.getY();
if (blockToCube(heightMax) < cubeY) {
return null;
}
int blockX = cubeToMinBlock(cube.getX()) + localX;
int blockZ = cubeToMinBlock(cube.getZ()) + localZ;
// If the given cube lies underneath the occluding block, it must be updated from the top down.
if (cubeY < blockToCube(heightMax)) {
// Determine the y-coordinate of the highest block (and its cube) occluding blocks inside of the given cube
// or further down.
int topBlockYInThisCubeOrBelow = getOcclusionHeightBelowCubeY(column, blockX, blockZ, cube.getY() + 1);
int topBlockCubeYInThisCubeOrBelow = blockToCube(topBlockYInThisCubeOrBelow);
// If the given cube contains the occluding block, the update can be limited down to that block.
if (topBlockCubeYInThisCubeOrBelow == cubeY) {
int heightBelowCube = getOcclusionHeightBelowCubeY(column, blockX, blockZ, cube.getY()) + 1;
return new ImmutablePair<>(heightBelowCube, cubeToMaxBlock(cubeY));
}
// Otherwise, the whole height of the cube must be updated.
else {
return new ImmutablePair<>(cubeToMinBlock(cubeY), cubeToMaxBlock(cubeY));
}
}
// ... otherwise, the update must start at the occluding block.
int heightBelowCube = getOcclusionHeightBelowCubeY(column, blockX, blockZ, cubeY);
return new ImmutablePair<>(heightBelowCube, heightMax);
}
}