functional weeping sky

This commit is contained in:
techno-sam
2025-08-31 22:11:58 +02:00
parent 4ff50b44d4
commit 82b6f661b9
25 changed files with 1789 additions and 17 deletions
+35 -6
View File
@@ -26,15 +26,44 @@ c fae net/minecraft/class_3572 net/minecraft/world/chunk/light/ChunkSkyLightProv
f J h field_44744 REMOVE_SKY_SOURCE_ENTRY f J h field_44744 REMOVE_SKY_SOURCE_ENTRY
f J i field_44745 ADD_SKY_SOURCE_ENTRY f J i field_44745 ADD_SKY_SOURCE_ENTRY
f Ljb$a; j field_44746 scratchPos f Ljb$a; j field_44746 scratchPos
m (IIII)V a method_51586 removeSourcesBelow
m (JLjh;IZI)V a method_51587 propagateFromEmptySections m (JLjh;IZI)V a method_51587 propagateFromEmptySections
p 0 blockPos p 1 blockPos
p 1 direction p 3 direction
p 2 lightLevel p 4 lightLevel
p 3 shouldIncrease p 5 shouldIncrease
p 4 emptySections p 6 emptySections
m (III)V b method_51590 updateSourcesInColumn m (III)V b method_51590 updateSourcesInColumn
p 1 x
p 2 z
p 3 lowestSourceY
v 4 15 -1 _ _ minY
m (IIII)V a method_51586 removeSourcesBelow
p 1 x
p 2 z
p 3 lowestSourceY
p 4 minY
v 5 13 -1 _ _ sectionX
v 6 19 -1 _ _ sectionZ
v 7 24 -1 _ _ topSolid
v 8 31 -1 _ _ sectionY
v 9 75 -1 _ _ minYInSection
v 10 82 -1 _ _ maxYInSection
v 11 91 -1 _ _ it_y
v 12 107 -1 _ _ it_pos
m (IIII)V b method_51591 addSourcesAbove m (IIII)V b method_51591 addSourcesAbove
p 1 x
p 2 z
p 3 lowestSourceY
p 4 minY
v 5 6 -1 _ _ sectionX
v 6 12 -1 _ _ sectionZ
v 7 63 -1 _ _ maxAdjacentLowestSourceY
v 8 71 -1 _ _ lowestRelevantY
v 9 85 -1 _ _ it_packedSection
v 11 125 -1 _ _ minYInSection
v 12 132 -1 _ _ maxYInSection
v 13 141 -1 _ _ it_y
v 14 157 -1 _ _ it_pos
m (J)V a method_51529 checkNode m (J)V a method_51529 checkNode
v 3 5 -1 _ _ x v 3 5 -1 _ _ x
v 4 11 -1 _ _ y v 4 11 -1 _ _ y
@@ -1,14 +1,47 @@
package io.github.slimeistdev.crystalline_sky; package io.github.slimeistdev.crystalline_sky;
import io.github.slimeistdev.crystalline_sky.extenders_cove.BlockRenderLayerExt; import io.github.slimeistdev.crystalline_sky.extenders_cove.BlockRenderLayerExt;
import io.github.slimeistdev.crystalline_sky.mixin_ducks.client.DebugRenderer_Duck;
import io.github.slimeistdev.crystalline_sky.registry.CrystallineBlocks; import io.github.slimeistdev.crystalline_sky.registry.CrystallineBlocks;
import net.fabricmc.api.ClientModInitializer; import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.fabricmc.fabric.api.client.rendering.v1.BlockRenderLayerMap; import net.fabricmc.fabric.api.client.rendering.v1.BlockRenderLayerMap;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.lwjgl.glfw.GLFW;
public class CrystallineSkyClient implements ClientModInitializer { public class CrystallineSkyClient implements ClientModInitializer {
@Override @Override
public void onInitializeClient() { public void onInitializeClient() {
BlockRenderLayerMap.putBlock(CrystallineBlocks.SKY, BlockRenderLayerExt.CRYSTALLINE_SKY_SKY); BlockRenderLayerMap.putBlock(CrystallineBlocks.SKY, BlockRenderLayerExt.CRYSTALLINE_SKY_SKY);
BlockRenderLayerMap.putBlock(CrystallineBlocks.WEEPING_SKY, BlockRenderLayerExt.CRYSTALLINE_SKY_SKY); BlockRenderLayerMap.putBlock(CrystallineBlocks.WEEPING_SKY, BlockRenderLayerExt.CRYSTALLINE_SKY_SKY);
var weepingSkyKeybind = KeyBindingHelper.registerKeyBinding(new KeyBinding(
"crystalline_sky.key.toggle_weeping_sky_debug",
InputUtil.Type.KEYSYM,
GLFW.GLFW_KEY_UNKNOWN,
"crystalline_sky.category.crystalline_sky"
));
ClientTickEvents.END_CLIENT_TICK.register(client -> {
while (weepingSkyKeybind.wasPressed()) {
int mode = ((DebugRenderer_Duck) client.debugRenderer).crystalline_sky$toggleWeepingSky();
Text message = getDebugMessage(Formatting.YELLOW,
Text.translatable("message.crystalline_sky.weeping_sky_debug." + mode)
);
client.inGameHud.getChatHud().addMessage(message);
}
});
}
@SuppressWarnings("SameParameterValue")
private static Text getDebugMessage(Formatting formatting, Text message) {
return Text.empty().append(Text.translatable("debug.prefix").formatted(formatting, Formatting.BOLD)).append(ScreenTexts.SPACE).append(message);
} }
} }
@@ -1,10 +1,12 @@
package io.github.slimeistdev.crystalline_sky; package io.github.slimeistdev.crystalline_sky;
import io.github.slimeistdev.crystalline_sky.registry.CrystallineBlocks; import io.github.slimeistdev.crystalline_sky.registry.CrystallineBlocks;
import io.github.slimeistdev.crystalline_sky.registry.CrystallineDataComponentTypes;
import io.github.slimeistdev.crystalline_sky.registry.CrystallineItems; import io.github.slimeistdev.crystalline_sky.registry.CrystallineItems;
public class ModSetup { public class ModSetup {
public static void init() { public static void init() {
CrystallineDataComponentTypes.init();
CrystallineBlocks.init(); CrystallineBlocks.init();
CrystallineItems.init(); CrystallineItems.init();
} }
@@ -0,0 +1,753 @@
package io.github.slimeistdev.crystalline_sky.infrastructure;
import com.google.common.base.Preconditions;
import io.github.slimeistdev.crystalline_sky.util.ArrayUtils;
import it.unimi.dsi.fastutil.shorts.ShortList;
import java.util.*;
import java.util.function.Consumer;
public class IntWeepingStorage implements WeepingStorage {
private final int[] sizes;
// each long is (y_sky << 32 | y_bottom)
// y_sky is the position of the highest weeping sky block in a layer
// y_bottom is the lowest illuminated position (so 1 above a solid block) (less than y_sky) in a layer, or -1 if the layer is unbounded
// must be sorted top-to-bottom by y_sky
private final int[][] columns;
private final int[] tempRunSplitSizes;
private final int[][] tempRunSplits;
// only used for iterators
private final int minY;
public IntWeepingStorage(int minY) {
this.sizes = new int[16*16];
this.columns = new int[16*16][0];
this.tempRunSplitSizes = new int[16*16*2];
this.tempRunSplits = new int[16*16*2][0];
this.minY = minY;
}
private static int getIndex(int localX, int localZ) {
return localX + localZ * 16;
}
private static int pack(short ySky, short yBottom) {
return ((int)ySky << 16) | Short.toUnsignedInt(yBottom);
}
private static short unpackSky(int packed) {
return (short)(packed >>> 16);
}
private static short unpackBottom(int packed) {
return (short)(packed & 0xFFFF);
}
private static int calculateIncreasedLength(int currentLength) {
if (currentLength == 0)
return 1;
// calculate next power of two
return Integer.highestOneBit(currentLength) << 1;
}
private void maybeShrinkColumn(int index) {
int[] column = columns[index];
int size = sizes[index];
int newLength = calculateIncreasedLength(size);
if (column.length > newLength) {
int[] newColumn = new int[newLength];
System.arraycopy(column, 0, newColumn, 0, size);
columns[index] = newColumn;
}
}
private void insertValue(int columnIndex, int i, int packed) {
int[] column = columns[columnIndex];
int size = sizes[columnIndex];
if (size + 1 > column.length) {
int newLength = calculateIncreasedLength(column.length);
int[] newColumn = new int[newLength];
System.arraycopy(column, 0, newColumn, 0, i);
newColumn[i] = packed;
System.arraycopy(column, i, newColumn, i + 1, size - i);
columns[columnIndex] = newColumn;
} else {
System.arraycopy(column, i, column, i + 1, size - i);
column[i] = packed;
}
sizes[columnIndex] = ++size;
}
/*
unlit sections are iterated downward
lit sections are iterated upward
a newly-unlit section should call insertTempRunSplit at its topY
a newly-lit section should call insertTempRunSplit at its bottomY
*/
/**
* Will split a run in subsequent iterators
* @param columnIndex column to split
* @param splitY the new (isLit ? bottomY : topY) of a run that intersects this one
* @param isLit whether this should be added to the splits for lit or unlit runs
*/
private void insertTempRunSplit(int columnIndex, int splitY, boolean isLit) {
int index = columnIndex * 2 + (isLit ? 1 : 0);
int size = tempRunSplitSizes[index];
int[] splits = tempRunSplits[index];
if (size == 0) {
if (splits.length == 0) {
tempRunSplits[index] = new int[] {splitY};
} else {
splits[0] = splitY;
}
tempRunSplitSizes[index] = ++size;
} else {
// binary search for insertion point
int insertIndex = ArrayUtils.binarySearchSortedInsertionPoint(splits, splitY, size);
if (insertIndex != -1) {
if (size + 1 > splits.length) {
int newLength = calculateIncreasedLength(splits.length);
int[] newSplits = new int[newLength];
System.arraycopy(splits, 0, newSplits, 0, insertIndex);
newSplits[insertIndex] = splitY;
System.arraycopy(splits, insertIndex, newSplits, insertIndex + 1, splits.length - insertIndex);
tempRunSplits[index] = newSplits;
} else {
System.arraycopy(splits, insertIndex, splits, insertIndex + 1, size - insertIndex);
splits[insertIndex] = splitY;
}
tempRunSplitSizes[index] = ++size;
}
}
}
private void litTempSplit(int columnIndex, int bottomY) {
insertTempRunSplit(columnIndex, bottomY, true);
}
private void unlitTempSplit(int columnIndex, int topY) {
insertTempRunSplit(columnIndex, topY, false);
}
@Override
public void insertSky(int localX, int y, int localZ, WeepingScanner scanner) {
int index = getIndex(localX, localZ);
int size = sizes[index];
int[] column = columns[index];
short yShort = (short) y;
if (size == 0) {
int solidY = scanner.scan(localX, y, localZ);
short shortLowestLitY = (short) (solidY == -1 ? -1 : solidY + 1);
int packed = pack(yShort, shortLowestLitY);
if (column.length == 0) {
columns[index] = new int[] {packed};
} else {
column[0] = packed;
}
sizes[index] = ++size;
litTempSplit(index, shortLowestLitY);
} else {
// possible cases:
// 1. we are in a layer, and do nothing
// 2. we are below the lowest layer, and create a new layer
// 3. we are above a layer, and must scan to determine whether to join it or insert one above it
for (int i = 0; i < size; i++) {
int packed = column[i];
short sky = unpackSky(packed);
short bottom = unpackBottom(packed);
if (yShort == sky)
return; // no change
if (bottom != -1 && yShort < sky && (yShort == bottom || yShort == bottom - 1)) {
// scan if layer can continue now, and extend it down if so. Else, fall through to in-or-below handling
if (!scanner.faceBlocksLight(localX, bottom, localZ)) {
int solidY = scanner.scan(localX, yShort, localZ);
short shortLowestLitY = (short) (solidY == -1 ? -1 : solidY + 1);
if (i + 1 < size) { // maybe join layer below
int packedBelow = column[i + 1];
short skyBelow = unpackSky(packedBelow);
short bottomBelow = unpackBottom(packedBelow);
if (solidY == -1 || solidY > skyBelow) { // join layer[i] and layer[i + 1]
litTempSplit(index, skyBelow + 1); // the newly-lit region is [skyBelow + 1, bottom - 1]
column[i] = pack(sky, bottomBelow);
System.arraycopy(column, i + 2, column, i + 1, size - (i + 2));
sizes[index] = --size;
maybeShrinkColumn(index);
return;
}
}
// extend layer[i] down
column[i] = pack(sky, shortLowestLitY);
litTempSplit(index, shortLowestLitY);
return;
}
}
if (yShort > sky) { // above layer
int solidY = scanner.scan(localX, y, localZ);
if (solidY == -1 || solidY < sky) { // join layer
column[i] = pack(yShort, bottom);
litTempSplit(index, sky + 1); // the newly-lit region is [sky + 1, yShort]
} else { // insert new layer above (at index i)
insertValue(index, i, pack(yShort, (short) (solidY + 1)));
litTempSplit(index, solidY + 1);
}
return;
} else { // (yShort < sky) in or below
if (bottom == -1 || yShort >= bottom) { // in layer, do nothing
return;
} else if (i == size - 1) { // below layer, add if last layer
int solidY = scanner.scan(localX, y, localZ);
short shortLowestLitY = (short) (solidY == -1 ? -1 : solidY + 1);
insertValue(index, size, pack(yShort, shortLowestLitY));
litTempSplit(index, shortLowestLitY);
return;
}
}
}
}
}
@Override
public void insertSolid(int localX, int y, int localZ, WeepingScanner scanner) {
int index = getIndex(localX, localZ);
int size = sizes[index];
int[] column = columns[index];
short yShort = (short) y;
// if we're inside a layer, limit it
boolean lightBlockedHere;
boolean lightBlockedAbove;
int lastLitY = -1;
boolean blockingCalculated = false;
for (int i = 0; i < size; i++) {
int packed = column[i];
short sky = unpackSky(packed);
short bottom = unpackBottom(packed);
if (yShort > sky) { // we're above all remaining layers, so nothing to do
return;
} else if (yShort == sky) { // either remove layer or lower the start (scan for sky)
int nextSky = scanner.scanForWeepingSky(localX, y, localZ);
if (nextSky == -1 || nextSky < bottom) { // remove layer
System.arraycopy(column, i + 1, column, i, size - (i + 1));
sizes[index] = --size;
maybeShrinkColumn(index);
} else { // lower start of layer
column[i] = pack((short) nextSky, bottom);
}
unlitTempSplit(index, sky);
return;
} else { // (yShort < sky) limit layer and potentially split layer
if (!blockingCalculated) {
lightBlockedHere = scanner.faceBlocksLight(localX, y, localZ);
lightBlockedAbove = scanner.faceBlocksLight(localX, y + 1, localZ);
if (lightBlockedAbove) {
lastLitY = y + 1;
} else if (lightBlockedHere) {
lastLitY = y;
} else { // no effect
return;
}
blockingCalculated = true;
}
if (lastLitY <= bottom) // no effect
continue;
int nextSky = scanner.scanForWeepingSky(localX, y, localZ);
// limit
column[i] = pack(sky, (short) lastLitY);
unlitTempSplit(index, lastLitY - 1);
if (nextSky != -1 && nextSky >= bottom) { // split
insertValue(index, i + 1, pack((short) nextSky, bottom));
}
return;
}
}
}
@Override
public void insertAir(int localX, int y, int localZ, WeepingScanner scanner) {
int index = getIndex(localX, localZ);
int size = sizes[index];
int[] column = columns[index];
short yShort = (short) y;
for (int i = 0; i < size; i++) {
int packed = column[i];
short sky = unpackSky(packed);
short bottom = unpackBottom(packed);
if (yShort > sky) { // above all remaining layers, nothing to do
return;
} else if (yShort == sky) { // rescan for sky
int nextSky = scanner.scanForWeepingSky(localX, y, localZ);
if (nextSky == -1 || nextSky < bottom) { // remove layer
System.arraycopy(column, i + 1, column, i, size - (i + 1));
sizes[index] = --size;
maybeShrinkColumn(index);
unlitTempSplit(index, sky);
} else { // lower start of layer
column[i] = pack((short) nextSky, bottom);
unlitTempSplit(index, sky);
}
return;
} else { // (yShort < sky) maybe extend layer down
if (bottom != -1 && (yShort == bottom || yShort == bottom - 1)) { // we may have freed things up
if (!scanner.faceBlocksLight(localX, bottom, localZ)) { // extend layer down or join with layer below
int solidY = scanner.scan(localX, yShort, localZ);
short shortLowestLitY = (short) (solidY == -1 ? -1 : solidY + 1);
if (i + 1 < size) { // maybe join layer below
int packedBelow = column[i + 1];
short skyBelow = unpackSky(packedBelow);
short bottomBelow = unpackBottom(packedBelow);
if (solidY == -1 || solidY < skyBelow) { // join layer[i] and layer[i + 1]
column[i] = pack(sky, bottomBelow);
System.arraycopy(column, i + 2, column, i + 1, size - (i + 2));
sizes[index] = --size;
litTempSplit(index, skyBelow + 1);
maybeShrinkColumn(index);
return;
}
}
// extend layer[i] down
column[i] = pack(sky, shortLowestLitY);
litTempSplit(index, shortLowestLitY);
return;
}
}
}
}
}
@Override
public void clear() {
int[] empty = new int[0];
for (int i = 0; i < 16*16; i++) {
sizes[i] = 0;
columns[i] = empty;
tempRunSplitSizes[i] = 0;
tempRunSplits[i] = empty;
}
}
public void set(int localX, int localZ, ShortList layers) {
Preconditions.checkArgument(layers.size() % 2 == 0, "Expected even number of shorts in layers");
int index = getIndex(localX, localZ);
int size = layers.size() / 2;
sizes[index] = size;
int[] column = columns[index];
if (column.length < size) {
column = new int[calculateIncreasedLength(size)];
columns[index] = column;
}
for (int i = 0; i < size; i++) {
short sky = layers.getShort(i * 2);
short bottom = layers.getShort(i * 2 + 1);
column[i] = pack(sky, bottom);
}
maybeShrinkColumn(index);
}
@Override
public boolean isLit(int localX, int y, int localZ) {
int index = getIndex(localX, localZ);
int size = sizes[index];
int[] column = columns[index];
if (size == 0)
return false;
short yShort = (short) (y - minY);
// binary search
int left = 0;
int right = size - 1;
while (left <= right) {
int mid = (left + right) / 2;
int packed = column[mid];
short sky = unpackSky(packed);
short bottom = unpackBottom(packed);
if (yShort > sky) { // we're higher
right = mid - 1;
} else if (yShort < sky) { // we're lower
if (bottom == -1 || yShort >= bottom) {
return true;
} else {
left = mid + 1;
}
} else { // yShort == sky
return true;
}
}
return false;
}
/**
* NOTE: unlike all other methods in this class, this uses global y coordinates.
*/
@Override
public Iterable<Run> iterateLitRuns(int localX, int localZ, int lowestSourceY) {
int index = getIndex(localX, localZ);
int size = sizes[index];
int[] column = columns[index];
int splitsColumnIndex = index * 2 + 1;
int splitsSize = tempRunSplitSizes[splitsColumnIndex];
int[] splits = tempRunSplits[splitsColumnIndex];
List<Run> runs = new ArrayList<>(size);
runs.add(new Run(lowestSourceY, Integer.MAX_VALUE));
if (lowestSourceY == Integer.MIN_VALUE) {
return wrapUnlitRuns(splitsSize, splits, runs);
}
for (int i = 0; i < size; i++) {
int packed = column[i];
short sky = unpackSky(packed);
short bottom = unpackBottom(packed);
int globalSky = sky + minY;
int globalBottom = bottom == -1 ? Integer.MIN_VALUE : bottom + minY;
// if we're above lowestSourceY, don't bother
if (globalBottom >= lowestSourceY)
continue;
// if we intersect with default sky, merge
if (globalSky >= lowestSourceY - 1) {
runs.set(0, new Run(globalBottom, Integer.MAX_VALUE));
continue;
}
// otherwise, add a new run
runs.add(new Run(globalBottom, globalSky));
}
return wrapLitRuns(splitsSize, splits, runs);
}
/**
* NOTE: unlike all other methods in this class, this uses global y coordinates.
*/
@Override
public Iterable<Run> iterateUnlitRuns(int localX, int localZ, int lowestSourceY) {
int index = getIndex(localX, localZ);
int size = sizes[index];
int[] column = columns[index];
int splitsColumnIndex = index * 2;
int splitsSize = tempRunSplitSizes[splitsColumnIndex];
int[] splits = tempRunSplits[splitsColumnIndex];
// fill with all gaps between layers, which are below lowestSourceY
List<Run> runs = new ArrayList<>(size);
if (lowestSourceY == Integer.MIN_VALUE) {
return wrapUnlitRuns(splitsSize, splits, runs);
}
if (size == 0) {
runs.add(new Run(Integer.MIN_VALUE, lowestSourceY - 1));
} else {
int lastLowestLit = lowestSourceY;
// each iteration adds the gap above layer[i]
for (int i = 0; i < size && lastLowestLit > Integer.MIN_VALUE; i++) {
int packed = column[i];
short sky = unpackSky(packed);
short bottom = unpackBottom(packed);
int globalSky = sky + minY;
int globalBottom = bottom == -1 ? Integer.MIN_VALUE : bottom + minY;
if (globalBottom >= lowestSourceY) // completely lit, what's the point?
continue;
if (globalSky < lastLowestLit - 1) // gap
runs.add(new Run(globalSky + 1, lastLowestLit - 1));
lastLowestLit = globalBottom;
}
// add the gap below the lowest layer
if (lastLowestLit > Integer.MIN_VALUE && lastLowestLit != lowestSourceY) {
runs.add(new Run(Integer.MIN_VALUE, lastLowestLit - 1));
}
}
return wrapUnlitRuns(splitsSize, splits, runs);
}
@Override
public void clearTempRunSplits(int localX, int localZ) {
int[] empty = new int[0];
int baseIndex = getIndex(localX, localZ) * 2;
for (int i = 0; i < 2; i++) {
int index = baseIndex + i;
tempRunSplitSizes[index] = 0;
tempRunSplits[index] = empty;
}
}
@Override
public boolean isColumnEmpty(int localX, int localZ) {
return sizes[getIndex(localX, localZ)] <= 0;
}
@Override
public boolean hasNoUnlitSplits(int localX, int localZ) {
return tempRunSplitSizes[getIndex(localX, localZ) * 2] <= 0;
}
@Override
public void debugState(int localX, int localZ, Consumer<String> stringConsumer) {
int index = getIndex(localX, localZ);
int size = sizes[index];
int[] column = columns[index];
if (size == 0) {
stringConsumer.accept(" <empty>");
} else {
for (int i = 0; i < size; i++) {
int packed = column[i];
short sky = unpackSky(packed);
short bottom = unpackBottom(packed);
stringConsumer.accept(" layer " + i + ": sky=" + (sky + minY) + ", bottom=" + (bottom == -1 ? "-1" : (bottom + minY)));
}
}
}
/** wrapped iterable must be sorted top-to-bottom, splits must be sorted bottom-to-top */
public Iterable<Run> wrapLitRuns(int splitsSize, int[] splits, Iterable<Run> iterable) {
if (splitsSize <= 0) return iterable;
return () -> new LitSplittingIterator(splitsSize, splits, iterable.iterator());
}
private class LitSplittingIterator implements Iterator<Run> {
private final int[] splits;
private final Iterator<Run> iterator;
private final Deque<Run> remainingSplits = new ArrayDeque<>();
private int currentSplitIndex;
private LitSplittingIterator(int splitsSize, int[] splits, Iterator<Run> iterator) {
this.splits = splits;
this.iterator = iterator;
this.currentSplitIndex = splitsSize - 1;
}
@Override
public boolean hasNext() {
return !remainingSplits.isEmpty() || iterator.hasNext();
}
@Override
public Run next() {
if (!remainingSplits.isEmpty()) {
return remainingSplits.removeFirst();
}
Run run = iterator.next();
while (currentSplitIndex >= 0) {
int splitY = splits[currentSplitIndex] + minY; // becomes the new bottomY of a run
if (splitY < run.bottomY()) { // split is below run, not relevant (yet)
return run;
} else if (splitY > run.topY()) { // split is above run, will never be relevant
currentSplitIndex--;
} else if (splitY == run.bottomY()) { // run already fulfills the split
currentSplitIndex--;
return run;
} else { // split is inside run, split it
Run upper = new Run(splitY, run.topY());
run = new Run(run.bottomY(), splitY - 1);
remainingSplits.addFirst(upper);
currentSplitIndex--;
}
}
return run;
}
}
/** wrapped iterable must be sorted top-to-bottom, splits must be sorted bottom-to-top */
private Iterable<Run> wrapUnlitRuns(int splitsSize, int[] splits, Iterable<Run> iterable) {
if (splitsSize <= 0) return iterable;
return () -> new UnlitSplittingIterator(splitsSize, splits, iterable.iterator());
}
private class UnlitSplittingIterator implements Iterator<Run> {
private final int[] splits;
private final Iterator<Run> iterator;
private final Deque<Run> remainingSplits = new ArrayDeque<>();
private int currentSplitIndex;
private UnlitSplittingIterator(int splitsSize, int[] splits, Iterator<Run> iterator) {
this.splits = splits;
this.iterator = iterator;
this.currentSplitIndex = splitsSize - 1;
}
@Override
public boolean hasNext() {
return !remainingSplits.isEmpty() || iterator.hasNext();
}
@Override
public Run next() {
if (!remainingSplits.isEmpty()) {
return remainingSplits.removeFirst();
}
Run run = iterator.next();
while (currentSplitIndex >= 0) {
int splitY = splits[currentSplitIndex] + minY; // becomes the new topY of a run
if (splitY < run.bottomY()) { // split is below run, not relevant (yet)
return run;
} else if (splitY > run.topY()) { // split is above run, will never be relevant
currentSplitIndex--;
return run;
} else if (splitY == run.topY()) { // run already fulfills the split
currentSplitIndex--;
return run;
} else { // split is inside run, split it
Run lower = new Run(run.bottomY(), splitY);
run = new Run(splitY + 1, run.topY());
remainingSplits.addFirst(lower);
currentSplitIndex--;
}
}
return run;
}
}
/*private class DownwardIterator implements IntIterator {
private final int[] column;
private final int size;
private int i;
private int currentY;
public DownwardIterator(int localX, int localZ, int yMax) {
int index = getIndex(localX, localZ);
int actualYMax = yMax - minY;
this.column = columns[index];
this.size = sizes[index];
this.i = -1;
// advance to first layer
for (int i = 0; i < size; i++) {
int packed = column[i];
short sky = unpackSky(packed);
short bottom = unpackBottom(packed);
if (bottom != -1 && bottom > actualYMax)
continue;
this.i = i;
currentY = Math.min(currentY, sky);
break;
}
}
@Override
public int nextInt() {
if (i == -1) {
throw new NoSuchElementException();
}
int ret = currentY;
int packed = column[i];
short bottom = unpackBottom(packed);
if (bottom != -1 && ret == bottom) {
i++;
if (i == size) {
i = -1;
} else {
packed = column[i];
currentY = unpackSky(packed);
}
} else {
currentY--;
}
return ret + minY;
}
@Override
public boolean hasNext() {
return i != -1;
}
}*/
}
@@ -0,0 +1,122 @@
package io.github.slimeistdev.crystalline_sky.infrastructure;
import io.github.slimeistdev.crystalline_sky.registry.CrystallineBlocks;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.shape.VoxelShape;
import net.minecraft.util.shape.VoxelShapes;
import net.minecraft.world.BlockView;
import net.minecraft.world.chunk.light.ChunkLightProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class MutableWeepingScanner implements WeepingStorage.WeepingScanner {
private @Nullable BlockView blockView;
private final int minY;
private final BlockPos.Mutable reusablePos1 = new BlockPos.Mutable();
private final BlockPos.Mutable reusablePos2 = new BlockPos.Mutable();
public MutableWeepingScanner(int minY) {
this.minY = minY;
}
public void setBlockView(@Nullable BlockView blockView) {
this.blockView = blockView;
}
private int transformInputY(int y) {
return y + minY;
}
private int transformOutputY(int y) {
return y == Integer.MIN_VALUE ? -1 : y - minY;
}
private @NotNull BlockView checkBlockView() {
if (blockView == null) {
throw new IllegalStateException("blockView is not set");
} else {
return blockView;
}
}
@Override
public int scan(int localX, int yMax, int localZ) {
BlockView blockView = checkBlockView();
int actualYMax = transformInputY(yMax);
BlockPos.Mutable topPos = reusablePos1.set(localX, actualYMax, localZ);
BlockPos.Mutable bottomPos = reusablePos2.set(localX, actualYMax - 1, localZ);
BlockState topState = blockView.getBlockState(topPos);
while (bottomPos.getY() >= minY) {
BlockState bottomState = blockView.getBlockState(bottomPos);
if (faceBlocksLight(topState, bottomState)) {
return transformOutputY(bottomPos.getY());
}
topState = bottomState;
topPos.set(bottomPos);
bottomPos.move(Direction.DOWN);
}
return -1;
}
@Override
public int scanForWeepingSky(int localX, int yMax, int localZ) {
BlockView blockView = checkBlockView();
int actualYMax = transformInputY(yMax);
BlockPos.Mutable topPos = reusablePos1.set(localX, actualYMax, localZ);
BlockPos.Mutable bottomPos = reusablePos2.set(localX, actualYMax - 1, localZ);
BlockState topState = blockView.getBlockState(topPos);
if (CrystallineBlocks.isWeepingSky(topState))
return yMax;
while (bottomPos.getY() >= minY) {
BlockState bottomState = blockView.getBlockState(bottomPos);
if (CrystallineBlocks.isWeepingSky(bottomState)) {
return transformOutputY(bottomPos.getY());
}
if (faceBlocksLight(topState, bottomState)) {
return -1;
}
topState = bottomState;
topPos.set(bottomPos);
bottomPos.move(Direction.DOWN);
}
return -1;
}
@Override
public boolean faceBlocksLight(int localX, int upperY, int localZ) {
BlockView blockView = checkBlockView();
int actualY = transformInputY(upperY);
reusablePos1.set(localX, actualY, localZ);
reusablePos2.set(localX, actualY - 1, localZ);
BlockState upper = blockView.getBlockState(reusablePos1);
BlockState lower = blockView.getBlockState(reusablePos2);
return faceBlocksLight(upper, lower);
}
private static boolean faceBlocksLight(BlockState upper, BlockState lower) {
if (lower.getOpacity() != 0) {
return true;
} else {
VoxelShape voxelShape = ChunkLightProvider.getOpaqueShape(upper, Direction.DOWN);
VoxelShape voxelShape2 = ChunkLightProvider.getOpaqueShape(lower, Direction.UP);
return VoxelShapes.unionCoversFullCube(voxelShape, voxelShape2);
}
}
}
@@ -0,0 +1,42 @@
package io.github.slimeistdev.crystalline_sky.infrastructure;
import it.unimi.dsi.fastutil.ints.IntIterator;
import java.util.NoSuchElementException;
public class PeekableIntIterator implements IntIterator {
private final IntIterator wrapped;
private boolean hasPeeked = false;
private int peekedValue;
public PeekableIntIterator(IntIterator wrapped) {
this.wrapped = wrapped;
}
public int peek() {
if (!hasPeeked) {
if (!wrapped.hasNext()) {
throw new NoSuchElementException();
}
peekedValue = wrapped.nextInt();
hasPeeked = true;
}
return peekedValue;
}
@Override
public int nextInt() {
if (hasPeeked) {
hasPeeked = false;
return peekedValue;
}
return wrapped.nextInt();
}
@Override
public boolean hasNext() {
return hasPeeked || wrapped.hasNext();
}
}
@@ -0,0 +1,53 @@
package io.github.slimeistdev.crystalline_sky.infrastructure;
import java.util.function.Consumer;
/**
* Note: y coordinates are [0, ...)
*/
public interface WeepingStorage {
void insertSky(int localX, int y, int localZ, WeepingScanner scanner);
void insertSolid(int localX, int y, int localZ, WeepingScanner scanner);
void insertAir(int localX, int y, int localZ, WeepingScanner scanner);
void clear();
boolean isLit(int localX, int y, int localZ);
Iterable<Run> iterateLitRuns(int localX, int localZ, int lowestSourceY);
Iterable<Run> iterateUnlitRuns(int localX, int localZ, int lowestSourceY);
void clearTempRunSplits(int localX, int localZ);
boolean isColumnEmpty(int localX, int localZ);
boolean hasNoUnlitSplits(int localX, int localZ);
void debugState(int localX, int localZ, Consumer<String> stringConsumer);
/** A continuous run of positions [bottomY, topY] */
record Run(int bottomY, int topY) {
public Run withBottomY(int newBottomY) {
return new Run(newBottomY, this.topY);
}
public Run withTopY(int newTopY) {
return new Run(this.bottomY, newTopY);
}
}
interface WeepingScanner {
/** Scans downwards from yMax (exclusive) to find the first solid block.
* Returns the y coordinate of the first solid block, or -1 if none was found.
* The returned value is guaranteed to be less than yMax.
*/
int scan(int localX, int yMax, int localZ);
int scanForWeepingSky(int localX, int yMax, int localZ);
boolean faceBlocksLight(int localX, int upperY, int localZ);
}
}
@@ -0,0 +1,143 @@
package io.github.slimeistdev.crystalline_sky.infrastructure.client;
import io.github.slimeistdev.crystalline_sky.infrastructure.WeepingStorage;
import io.github.slimeistdev.crystalline_sky.mixin_ducks.ChunkSkyLight_Duck;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.debug.DebugRenderer;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.util.math.ColumnPos;
import net.minecraft.world.chunk.light.ChunkSkyLight;
import net.minecraft.world.chunk.light.LightSourceView;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
@Environment(EnvType.CLIENT)
public class WeepingSkyDebugRenderer implements DebugRenderer.Renderer {
private final MinecraftClient client;
private @Nullable ColumnPos column = null;
private boolean litMode = true;
private static final int[] COLORS = {
0xFFFF0000,
0xFFFF7F00,
0xFFFFFF00,
0xFF00FF00,
0xFF00FFFF,
0xFF007FFF,
0xFF0000FF,
0xFF7F00FF,
0xFFFF00FF,
0xFFFF007F,
};
public WeepingSkyDebugRenderer(MinecraftClient client) {
this.client = client;
}
public void setColumn(@Nullable ColumnPos column) {
this.column = column;
}
public void setLitMode(boolean lit) {
this.litMode = lit;
}
@Override
public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, double cameraX, double cameraY, double cameraZ) {
if (column == null)
return;
if (client.world == null)
return;
ChunkPos chunkPos = column.toChunkPos();
LightSourceView chunk = client.world.getChunkManager().getChunk(chunkPos.x, chunkPos.z);
if (chunk == null)
return;
ChunkSkyLight chunkSkyLight = chunk.getChunkSkyLight();
WeepingStorage weepingStorage = ((ChunkSkyLight_Duck) chunkSkyLight).crystalline_sky$getWeepingStorage();
if (weepingStorage == null)
return;
int localX = column.x() & 15;
int localZ = column.z() & 15;
int lowestSourceY = chunkSkyLight.get(localX, localZ);
Matrix4f matrix4f = matrices.peek().getPositionMatrix();
float x1 = (float) (column.x() - cameraX);
float x2 = x1 + 1;
float z1 = (float) (column.z() - cameraZ);
float z2 = z1 + 1;
int colorIndex = 0;
Iterable<WeepingStorage.Run> runs = this.litMode
? weepingStorage.iterateLitRuns(localX, localZ, lowestSourceY)
: weepingStorage.iterateUnlitRuns(localX, localZ, lowestSourceY);
for (WeepingStorage.Run run : runs) {
int color = COLORS[colorIndex++ % COLORS.length];
// limit things to a renderable range
int bottomY = Math.max(-0xFFFF, run.bottomY());
int topY = Math.min(run.topY(), 0xFFFF) + 1;
float y1 = (float) (bottomY - cameraY);
float y2 = (float) (topY - cameraY);
// draw outlined box
renderOutlinedBox(
vertexConsumers, matrix4f,
x1, x2,
y1, y2,
z1, z2,
color
);
}
}
private static void renderOutlinedBox(VertexConsumerProvider vertexConsumers, Matrix4f matrix, float x1, float x2, float y1, float y2, float z1, float z2, int color) {
VertexConsumer vertexConsumer = vertexConsumers.getBuffer(RenderLayer.getDebugLineStrip(2.0));
// render top face
renderHorizontalSquare(matrix, x1, x2, y1, z1, z2, color, vertexConsumer);
// render bottom face
renderHorizontalSquare(matrix, x1, x2, y2, z1, z2, color, vertexConsumer);
// the (x1, z1) vertical edge has been drawn, draw the other 3
// (x2, z1)
vertexConsumer.vertex(matrix, x2, y2, z1).color(color);
vertexConsumer.vertex(matrix, x2, y1, z1).color(color);
// (x2, z2)
vertexConsumer.vertex(matrix, x2, y1, z2).color(color);
vertexConsumer.vertex(matrix, x2, y2, z2).color(color);
// (x1, z2)
vertexConsumer.vertex(matrix, x1, y2, z2).color(color);
vertexConsumer.vertex(matrix, x1, y1, z2).color(color);
}
private static void renderHorizontalSquare(Matrix4f matrix, float x1, float x2, float y, float z1, float z2, int color, VertexConsumer vertexConsumer) {
vertexConsumer.vertex(matrix, x1, y, z1).color(color);
vertexConsumer.vertex(matrix, x2, y, z1).color(color);
vertexConsumer.vertex(matrix, x2, y, z2).color(color);
vertexConsumer.vertex(matrix, x1, y, z2).color(color);
vertexConsumer.vertex(matrix, x1, y, z1).color(color);
}
@Override
public void clear() {
this.column = null;
}
}
@@ -0,0 +1,46 @@
package io.github.slimeistdev.crystalline_sky.items;
import io.github.slimeistdev.crystalline_sky.registry.CrystallineDataComponentTypes;
import net.minecraft.block.Block;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.BlockItem;
import net.minecraft.item.ItemStack;
import net.minecraft.item.ItemUsageContext;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.ColumnPos;
import net.minecraft.world.World;
public class WeepingSkyBlockItem extends BlockItem {
public WeepingSkyBlockItem(Block block, Settings settings) {
super(block, settings);
}
@Override
public ActionResult useOnBlock(ItemUsageContext context) {
ItemStack stack = context.getStack();
if (stack.get(CrystallineDataComponentTypes.WEEPING_SKY_DEBUG_COLUMN_TOOL) != null) {
if (context.getWorld().isClient) {
BlockPos pos = context.getBlockPos();
WeepingSkyBlockItemClient.setSelectedColumn(new ColumnPos(pos.getX(), pos.getZ()));
}
return ActionResult.SUCCESS;
}
return super.useOnBlock(context);
}
@Override
public ActionResult use(World world, PlayerEntity user, Hand hand) {
ItemStack stack = user.getStackInHand(hand);
if (stack.get(CrystallineDataComponentTypes.WEEPING_SKY_DEBUG_COLUMN_TOOL) != null) {
if (world.isClient) {
WeepingSkyBlockItemClient.setSelectedColumn(null);
}
return ActionResult.SUCCESS;
}
return super.use(world, user, hand);
}
}
@@ -0,0 +1,25 @@
package io.github.slimeistdev.crystalline_sky.items;
import io.github.slimeistdev.crystalline_sky.infrastructure.client.WeepingSkyDebugRenderer;
import io.github.slimeistdev.crystalline_sky.mixin_ducks.client.DebugRenderer_Duck;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.text.Text;
import net.minecraft.util.math.ColumnPos;
import org.jetbrains.annotations.Nullable;
public class WeepingSkyBlockItemClient {
// todo check if this is server-safe
@Environment(EnvType.CLIENT)
public static void setSelectedColumn(@Nullable ColumnPos column) {
MinecraftClient mc = MinecraftClient.getInstance();
WeepingSkyDebugRenderer weepingSkyDebugRenderer = ((DebugRenderer_Duck) mc.debugRenderer).crystalline_sky$getWeepingSkyDebugRenderer();
weepingSkyDebugRenderer.setColumn(column);
Text text = column == null
? Text.translatable("message.crystalline_sky.weeping_sky_debug.column.cleared")
: Text.translatable("message.crystalline_sky.weeping_sky_debug.column.set", column.x(), column.z());
mc.inGameHud.setOverlayMessage(text, false);
}
}
@@ -11,6 +11,8 @@ import org.spongepowered.asm.mixin.Mixin;
public class ChunkLightProviderMixin { public class ChunkLightProviderMixin {
@WrapMethod(method = "needsLightUpdate") @WrapMethod(method = "needsLightUpdate")
private static boolean updateSky(BlockState oldState, BlockState newState, Operation<Boolean> original) { private static boolean updateSky(BlockState oldState, BlockState newState, Operation<Boolean> original) {
return original.call(oldState, newState) || (CrystallineBlocks.getSkyLightLevel(oldState) != CrystallineBlocks.getSkyLightLevel(newState)); return original.call(oldState, newState)
|| (CrystallineBlocks.getSkyLightLevel(oldState) != CrystallineBlocks.getSkyLightLevel(newState))
|| (CrystallineBlocks.isWeepingSky(oldState) != CrystallineBlocks.isWeepingSky(newState));
} }
} }
@@ -0,0 +1,151 @@
package io.github.slimeistdev.crystalline_sky.mixin;
import io.github.slimeistdev.crystalline_sky.CrystallineSky;
import io.github.slimeistdev.crystalline_sky.infrastructure.IntWeepingStorage;
import io.github.slimeistdev.crystalline_sky.infrastructure.MutableWeepingScanner;
import io.github.slimeistdev.crystalline_sky.infrastructure.WeepingStorage;
import io.github.slimeistdev.crystalline_sky.mixin_ducks.ChunkSkyLight_Duck;
import io.github.slimeistdev.crystalline_sky.registry.CrystallineBlocks;
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import it.unimi.dsi.fastutil.shorts.ShortList;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.ChunkSectionPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.BlockView;
import net.minecraft.world.HeightLimitView;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.ChunkSection;
import net.minecraft.world.chunk.light.ChunkSkyLight;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ChunkSkyLight.class)
public abstract class ChunkSkyLightMixin implements ChunkSkyLight_Duck {
@Shadow
@Final
private int minY;
@Shadow
@Final
private BlockPos.Mutable reusableBlockPos1;
@Shadow
private static boolean faceBlocksLight(BlockState upper, BlockState lower) {
throw new RuntimeException("Mixin failed to apply");
}
@Shadow
@Final
private BlockPos.Mutable reusableBlockPos2;
@Unique
@Final
@Mutable
private IntWeepingStorage crystalline_sky$weepingStorage;
@Unique
@Final
@Mutable
private MutableWeepingScanner crystalline_sky$weepingScanner;
@Inject(method = "<init>", at = @At("RETURN"))
private void init(HeightLimitView heightLimitView, CallbackInfo ci) {
crystalline_sky$weepingStorage = new IntWeepingStorage(minY + 1);
crystalline_sky$weepingScanner = new MutableWeepingScanner(minY + 1);
}
@Override
public WeepingStorage crystalline_sky$getWeepingStorage() {
return crystalline_sky$weepingStorage;
}
@Inject(method = "refreshSurfaceY", at = @At("HEAD"))
private void refreshWeepingSurfaceY(Chunk chunk, CallbackInfo ci) {
int highestNonEmptySectionIndex = chunk.getHighestNonEmptySection();
if (highestNonEmptySectionIndex == -1) {
crystalline_sky$weepingStorage.clear();
} else {
int topY = ChunkSectionPos.getBlockCoord(chunk.sectionIndexToCoord(highestNonEmptySectionIndex) + 1);
int weepingMinY = minY + 1;
ShortList data = new ShortArrayList();
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
data.clear();
BlockPos.Mutable topPos = reusableBlockPos1.set(x, topY, z);
BlockPos.Mutable bottomPos = reusableBlockPos2.set(x, topY - 1, z);
BlockState topState = Blocks.AIR.getDefaultState();
short foundSky = -1;
for (int sectionIndex = highestNonEmptySectionIndex; sectionIndex >= 0; sectionIndex--) {
ChunkSection section = chunk.getSection(sectionIndex);
if (section.isEmpty()) {
topState = Blocks.AIR.getDefaultState();
topPos.setY(ChunkSectionPos.getBlockCoord(chunk.sectionIndexToCoord(sectionIndex)));
bottomPos.setY(topPos.getY() - 1);
} else {
for (int y = 15; y >= 0; y--) {
BlockState bottomState = section.getBlockState(x, y, z);
if (CrystallineBlocks.isWeepingSky(bottomState)) {
if (foundSky == -1) {
foundSky = (short) (bottomPos.getY() - weepingMinY);
}
} else if (faceBlocksLight(topState, bottomState)) {
if (foundSky != -1) {
short lastLitY = (short) (bottomPos.getY() - weepingMinY + 1);
data.add(foundSky);
data.add(lastLitY);
foundSky = -1;
}
}
topState = bottomState;
topPos.set(bottomPos);
bottomPos.move(Direction.DOWN);
}
}
}
if (foundSky != -1) {
data.add(foundSky);
data.add((short) -1);
}
crystalline_sky$weepingStorage.set(x, z, data);
}
}
}
}
@Inject(method = "isSkyLightAccessible(Lnet/minecraft/world/BlockView;III)Z", at = @At("HEAD"))
private void updateWeepingSkyState(BlockView blockView, int localX, int y, int localZ, CallbackInfoReturnable<Boolean> cir) {
BlockPos pos = reusableBlockPos1.set(localX, y, localZ);
BlockState state = blockView.getBlockState(pos);
crystalline_sky$weepingScanner.setBlockView(blockView);
int weepingMinY = minY + 1;
if (state.isAir()) {
crystalline_sky$weepingStorage.insertAir(localX, y - weepingMinY, localZ, crystalline_sky$weepingScanner);
} else if (CrystallineBlocks.isWeepingSky(state)) {
crystalline_sky$weepingStorage.insertSky(localX, y - weepingMinY, localZ, crystalline_sky$weepingScanner);
} else if (state.isOpaque()) {
crystalline_sky$weepingStorage.insertSolid(localX, y - weepingMinY, localZ, crystalline_sky$weepingScanner);
}
CrystallineSky.LOG.info("Weeping sky ranges for local column [{}, {}] in {}:", localX, localZ, blockView);
crystalline_sky$weepingStorage.debugState(localX, localZ, CrystallineSky.LOG::info);
crystalline_sky$weepingScanner.setBlockView(null);
}
}
@@ -1,14 +1,23 @@
package io.github.slimeistdev.crystalline_sky.mixin; package io.github.slimeistdev.crystalline_sky.mixin;
import com.llamalad7.mixinextras.expression.Definition;
import com.llamalad7.mixinextras.expression.Expression;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import io.github.slimeistdev.crystalline_sky.infrastructure.WeepingStorage;
import io.github.slimeistdev.crystalline_sky.mixin_ducks.ChunkSkyLight_Duck;
import io.github.slimeistdev.crystalline_sky.registry.CrystallineBlocks; import io.github.slimeistdev.crystalline_sky.registry.CrystallineBlocks;
import net.minecraft.block.BlockState; import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.ChunkPos; import net.minecraft.util.math.ChunkPos;
import net.minecraft.util.math.ChunkSectionPos; import net.minecraft.util.math.ChunkSectionPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.ChunkProvider; import net.minecraft.world.chunk.ChunkProvider;
import net.minecraft.world.chunk.ChunkToNibbleArrayMap; import net.minecraft.world.chunk.ChunkToNibbleArrayMap;
import net.minecraft.world.chunk.light.ChunkLightProvider; import net.minecraft.world.chunk.light.ChunkLightProvider;
import net.minecraft.world.chunk.light.ChunkSkyLight;
import net.minecraft.world.chunk.light.ChunkSkyLightProvider; import net.minecraft.world.chunk.light.ChunkSkyLightProvider;
import net.minecraft.world.chunk.light.LightSourceView; import net.minecraft.world.chunk.light.LightSourceView;
import net.minecraft.world.chunk.light.LightStorage; import net.minecraft.world.chunk.light.LightStorage;
@@ -19,12 +28,30 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
// todo replace all name = "..." locals with ordinal
@Mixin(ChunkSkyLightProvider.class) @Mixin(ChunkSkyLightProvider.class)
public abstract class ChunkSkyLightProviderMixin<M extends ChunkToNibbleArrayMap<M>, S extends LightStorage<M>> extends ChunkLightProvider<M, S> { public abstract class ChunkSkyLightProviderMixin<M extends ChunkToNibbleArrayMap<M>, S extends LightStorage<M>> extends ChunkLightProvider<M, S> {
@Shadow @Shadow
@Final @Final
private BlockPos.Mutable scratchPos; private BlockPos.Mutable scratchPos;
@Shadow
private static boolean isMaxLightLevel(int lightLevel) {
throw new RuntimeException("Mixin failed to apply");
}
@Shadow
@Final
private static long REMOVE_TOP_SKY_SOURCE_ENTRY;
@Shadow
@Final
private static long REMOVE_SKY_SOURCE_ENTRY;
@Shadow
@Final
private static long ADD_SKY_SOURCE_ENTRY;
protected ChunkSkyLightProviderMixin(ChunkProvider chunkProvider, S lightStorage) { protected ChunkSkyLightProviderMixin(ChunkProvider chunkProvider, S lightStorage) {
super(chunkProvider, lightStorage); super(chunkProvider, lightStorage);
} }
@@ -32,12 +59,38 @@ public abstract class ChunkSkyLightProviderMixin<M extends ChunkToNibbleArrayMap
@Inject(method = "checkNode", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/chunk/light/SkyLightStorage;get(J)I")) @Inject(method = "checkNode", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/chunk/light/SkyLightStorage;get(J)I"))
private void emitFromCrystallineSky(long blockPos, CallbackInfo ci) { private void emitFromCrystallineSky(long blockPos, CallbackInfo ci) {
BlockState state = getStateForLighting(scratchPos.set(blockPos)); BlockState state = getStateForLighting(scratchPos.set(blockPos));
int level = CrystallineBlocks.getSkyLightLevel(state); int level = CrystallineBlocks.isWeepingSky(state)
? 15
: CrystallineBlocks.getSkyLightLevel(state);
if (level > 0 && ((LightStorageAccessor) lightStorage).crystalline_sky$callIsSectionInEnabledColumn(ChunkSectionPos.fromBlockPos(blockPos))) { if (level > 0 && ((LightStorageAccessor) lightStorage).crystalline_sky$callIsSectionInEnabledColumn(ChunkSectionPos.fromBlockPos(blockPos))) {
enqueueIncrease(blockPos, QueueEntry.increaseLightFromEmission(level, isTrivialForLighting(state))); enqueueIncrease(blockPos, QueueEntry.increaseLightFromEmission(level, isTrivialForLighting(state)));
} }
} }
@Definition(id = "y", local = @Local(type = int.class, name = "y"))
@Definition(id = "lowestSourceY", local = @Local(type = int.class, name = "lowestSourceY"))
@Expression("y >= lowestSourceY")
@WrapOperation(method = "checkNode", at = @At("MIXINEXTRAS:EXPRESSION"))
private boolean weepBelowTheLowest(int y, int lowestSourceY, Operation<Boolean> original, @Local(name = "x") int x, @Local(name = "z") int z) {
if (original.call(y, lowestSourceY)) {
return true;
}
LightSourceView chunk = chunkProvider.getChunk(ChunkSectionPos.getSectionCoord(x), ChunkSectionPos.getSectionCoord(z));
if (chunk == null) return false;
ChunkSkyLight chunkSkyLight = chunk.getChunkSkyLight();
if (chunkSkyLight == null) return false;
int localX = ChunkSectionPos.getLocalCoord(x);
int localZ = ChunkSectionPos.getLocalCoord(z);
WeepingStorage weepingStorage = ((ChunkSkyLight_Duck) chunkSkyLight).crystalline_sky$getWeepingStorage();
if (weepingStorage.isColumnEmpty(localX, localZ)) return false;
return weepingStorage.isLit(localX, y, localZ);
}
@Inject(method = "propagateLight", at = @At("RETURN")) @Inject(method = "propagateLight", at = @At("RETURN"))
private void propagateCrystallineSkyLight(ChunkPos chunkPos, CallbackInfo ci) { private void propagateCrystallineSkyLight(ChunkPos chunkPos, CallbackInfo ci) {
LightSourceView lightSourceView = this.chunkProvider.getChunk(chunkPos.x, chunkPos.z); LightSourceView lightSourceView = this.chunkProvider.getChunk(chunkPos.x, chunkPos.z);
@@ -47,4 +100,136 @@ public abstract class ChunkSkyLightProviderMixin<M extends ChunkToNibbleArrayMap
enqueueIncrease(pos.asLong(), QueueEntry.increaseLightFromEmission(CrystallineBlocks.getSkyLightLevel(state), isTrivialForLighting(state))); enqueueIncrease(pos.asLong(), QueueEntry.increaseLightFromEmission(CrystallineBlocks.getSkyLightLevel(state), isTrivialForLighting(state)));
}); });
} }
@Inject(method = "removeSourcesBelow", at = @At("HEAD"), cancellable = true)
private void removeWeepingSourcesBelow(int x, int z, int lowestSourceY, int minY, CallbackInfo ci) {
LightSourceView chunk = chunkProvider.getChunk(ChunkSectionPos.getSectionCoord(x), ChunkSectionPos.getSectionCoord(z));
if (chunk == null) return;
ChunkSkyLight chunkSkyLight = chunk.getChunkSkyLight();
if (chunkSkyLight == null) return;
int localX = ChunkSectionPos.getLocalCoord(x);
int localZ = ChunkSectionPos.getLocalCoord(z);
WeepingStorage weepingStorage = ((ChunkSkyLight_Duck) chunkSkyLight).crystalline_sky$getWeepingStorage();
if (weepingStorage.isColumnEmpty(localX, localZ) && weepingStorage.hasNoUnlitSplits(localX, localZ)) return;
// there's little chance of accidental mixin compatibility anyway, so just cancel
ci.cancel();
int sectionX = ChunkSectionPos.getSectionCoord(x);
int sectionZ = ChunkSectionPos.getSectionCoord(z);
int effectiveLowestSourceY = lowestSourceY <= minY ? Integer.MIN_VALUE : lowestSourceY;
SkyLightStorageAccessor lightStorageAccessor = (SkyLightStorageAccessor) this.lightStorage;
RunIter: for (WeepingStorage.Run run : weepingStorage.iterateUnlitRuns(localX, localZ, effectiveLowestSourceY)) {
// check lighting state of run
int topUnlit = run.topY();
int bottomUnlit = run.bottomY();
for (int sectionY = ChunkSectionPos.getSectionCoord(topUnlit); lightStorageAccessor.crystalline_sky$isAboveMinHeight(sectionY); sectionY--) {
if (lightStorageAccessor.crystalline_sky$hasSection(ChunkSectionPos.asLong(sectionX, sectionY, sectionZ))) {
int minYInSection = ChunkSectionPos.getBlockCoord(sectionY);
int maxYInSection = minYInSection + 15;
if (maxYInSection < bottomUnlit) { // todone break outer early if we're below bottomUnlit
break;
}
for (int it_y = Math.min(maxYInSection, topUnlit); it_y >= Math.max(minYInSection, bottomUnlit); it_y--) {
long it_pos = BlockPos.asLong(x, it_y, z);
if (CrystallineBlocks.isCrystallineSky(chunk.getBlockState(scratchPos.set(it_pos)))) {
continue;
}
if (!isMaxLightLevel(lightStorageAccessor.crystalline_sky$get(it_pos))) {
continue RunIter;
}
lightStorageAccessor.crystalline_sky$set(it_pos, 0);
// todone which of these is correct?
enqueueDecrease(it_pos, it_y == topUnlit ? REMOVE_TOP_SKY_SOURCE_ENTRY : REMOVE_SKY_SOURCE_ENTRY);
//enqueueDecrease(it_pos, REMOVE_SKY_SOURCE_ENTRY);
}
}
}
}
}
@Inject(method = "addSourcesAbove", at = @At("RETURN"))
private void addAdditionalWeepingSourcesAbove(int x, int z, int lowestSourceY, int minY, CallbackInfo ci,
@Local(name = "maxAdjacentLowestSourceY") int maxAdjacentLowestSourceY) {
LightSourceView chunk = chunkProvider.getChunk(ChunkSectionPos.getSectionCoord(x), ChunkSectionPos.getSectionCoord(z));
if (chunk == null) return;
ChunkSkyLight chunkSkyLight = chunk.getChunkSkyLight();
if (chunkSkyLight == null) return;
int localX = ChunkSectionPos.getLocalCoord(x);
int localZ = ChunkSectionPos.getLocalCoord(z);
WeepingStorage weepingStorage = ((ChunkSkyLight_Duck) chunkSkyLight).crystalline_sky$getWeepingStorage();
if (weepingStorage.isColumnEmpty(localX, localZ)) {
weepingStorage.clearTempRunSplits(localX, localZ);
return;
}
int sectionX = ChunkSectionPos.getSectionCoord(x);
int sectionZ = ChunkSectionPos.getSectionCoord(z);
int effectiveLowestSourceY = lowestSourceY <= minY ? Integer.MIN_VALUE : lowestSourceY;
if (effectiveLowestSourceY == Integer.MIN_VALUE) {
weepingStorage.clearTempRunSplits(localX, localZ);
return;
}
SkyLightStorageAccessor lightStorageAccessor = (SkyLightStorageAccessor) this.lightStorage;
RunIter: for (WeepingStorage.Run run : weepingStorage.iterateLitRuns(localX, localZ, effectiveLowestSourceY)) {
if (run.topY() == Integer.MAX_VALUE) {
if (run.bottomY() == effectiveLowestSourceY) {
continue;
} else {
run = run.withTopY(effectiveLowestSourceY - 1);
}
}
int lowestRelevantY = Math.max(run.bottomY(), minY);
for (long it_packedSection = ChunkSectionPos.asLong(sectionX, ChunkSectionPos.getSectionCoord(lowestRelevantY), sectionZ);
!lightStorageAccessor.crystalline_sky$isAtOrAboveTopmostSection(it_packedSection);
it_packedSection = ChunkSectionPos.offset(it_packedSection, Direction.UP)
) {
if (lightStorageAccessor.crystalline_sky$hasSection(it_packedSection)) {
int minYInSection = ChunkSectionPos.getBlockCoord(ChunkSectionPos.unpackY(it_packedSection));
int maxYInSection = minYInSection + 15;
if (minYInSection > run.topY()) { // todone break outer early if we're above run.topY()
break;
}
for (int it_y = Math.max(minYInSection, lowestRelevantY); it_y <= Math.min(maxYInSection, run.topY()); it_y++) {
long it_pos = BlockPos.asLong(x, it_y, z);
if (CrystallineBlocks.isCrystallineSky(chunk.getBlockState(scratchPos.set(it_pos)))) {
continue;
}
if (isMaxLightLevel(lightStorageAccessor.crystalline_sky$get(it_pos))) {
continue RunIter;
}
lightStorageAccessor.crystalline_sky$set(it_pos, 15);
if (it_y < maxAdjacentLowestSourceY || it_y == lowestRelevantY) {
enqueueIncrease(it_pos, ADD_SKY_SOURCE_ENTRY);
}
}
}
}
}
weepingStorage.clearTempRunSplits(localX, localZ);
}
} }
@@ -8,4 +8,13 @@ import org.spongepowered.asm.mixin.gen.Invoker;
public interface LightStorageAccessor { public interface LightStorageAccessor {
@Invoker("isSectionInEnabledColumn") @Invoker("isSectionInEnabledColumn")
boolean crystalline_sky$callIsSectionInEnabledColumn(long sectionPos); boolean crystalline_sky$callIsSectionInEnabledColumn(long sectionPos);
@Invoker("hasSection")
boolean crystalline_sky$hasSection(long sectionPos);
@Invoker("get")
int crystalline_sky$get(long blockPos);
@Invoker("set")
void crystalline_sky$set(long blockPos, int lightLevel);
} }
@@ -0,0 +1,14 @@
package io.github.slimeistdev.crystalline_sky.mixin;
import net.minecraft.world.chunk.light.SkyLightStorage;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(SkyLightStorage.class)
public interface SkyLightStorageAccessor extends LightStorageAccessor {
@Invoker("isAboveMinHeight")
boolean crystalline_sky$isAboveMinHeight(int sectionY);
@Invoker("isAtOrAboveTopmostSection")
boolean crystalline_sky$isAtOrAboveTopmostSection(long sectionPos);
}
@@ -0,0 +1,56 @@
package io.github.slimeistdev.crystalline_sky.mixin.client;
import io.github.slimeistdev.crystalline_sky.infrastructure.client.WeepingSkyDebugRenderer;
import io.github.slimeistdev.crystalline_sky.mixin_ducks.client.DebugRenderer_Duck;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.Frustum;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.debug.DebugRenderer;
import net.minecraft.client.util.math.MatrixStack;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(DebugRenderer.class)
public class DebugRendererMixin implements DebugRenderer_Duck {
@Unique
@Mutable
@Final
private WeepingSkyDebugRenderer crystalline_sky$weepingSkyDebugRenderer;
@Unique
private int crystalline_sky$weepingSkyMode = 0;
@Override
public WeepingSkyDebugRenderer crystalline_sky$getWeepingSkyDebugRenderer() {
return crystalline_sky$weepingSkyDebugRenderer;
}
@Override
public int crystalline_sky$toggleWeepingSky() {
crystalline_sky$weepingSkyMode = (crystalline_sky$weepingSkyMode + 1) % 3;
return crystalline_sky$weepingSkyMode;
}
@Inject(method = "<init>", at = @At("RETURN"))
private void init(MinecraftClient client, CallbackInfo ci) {
this.crystalline_sky$weepingSkyDebugRenderer = new WeepingSkyDebugRenderer(client);
}
@Inject(method = "render", at = @At("RETURN"))
private void render(MatrixStack matrices, Frustum frustum, VertexConsumerProvider.Immediate vertexConsumers, double cameraX, double cameraY, double cameraZ, CallbackInfo ci) {
if (crystalline_sky$weepingSkyMode != 0) {
crystalline_sky$weepingSkyDebugRenderer.setLitMode(crystalline_sky$weepingSkyMode == 1);
crystalline_sky$weepingSkyDebugRenderer.render(matrices, vertexConsumers, cameraX, cameraY, cameraZ);
}
}
@Inject(method = "reset", at = @At("RETURN"))
private void reset(CallbackInfo ci) {
crystalline_sky$weepingSkyDebugRenderer.clear();
}
}
@@ -158,7 +158,9 @@ public abstract class WorldRendererMixin {
if (blockRenderLayer == BlockRenderLayerExt.CRYSTALLINE_SKY_SKY if (blockRenderLayer == BlockRenderLayerExt.CRYSTALLINE_SKY_SKY
&& client.player != null && client.player != null
&& (client.player.getMainHandStack().isOf(CrystallineItems.SKY) && (client.player.getMainHandStack().isOf(CrystallineItems.SKY)
|| client.player.getOffHandStack().isOf(CrystallineItems.SKY))) { || client.player.getOffHandStack().isOf(CrystallineItems.SKY)
|| client.player.getMainHandStack().isOf(CrystallineItems.WEEPING_SKY)
|| client.player.getOffHandStack().isOf(CrystallineItems.WEEPING_SKY))) {
float f = ticks + client.getRenderTickCounter().getTickProgress(true); float f = ticks + client.getRenderTickCounter().getTickProgress(true);
float alpha = (MathHelper.sin(f / 10.0f) + 1.0f) / 2.0f; float alpha = (MathHelper.sin(f / 10.0f) + 1.0f) / 2.0f;
@@ -0,0 +1,7 @@
package io.github.slimeistdev.crystalline_sky.mixin_ducks;
import io.github.slimeistdev.crystalline_sky.infrastructure.WeepingStorage;
public interface ChunkSkyLight_Duck {
WeepingStorage crystalline_sky$getWeepingStorage();
}
@@ -0,0 +1,12 @@
package io.github.slimeistdev.crystalline_sky.mixin_ducks.client;
import io.github.slimeistdev.crystalline_sky.infrastructure.client.WeepingSkyDebugRenderer;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
@Environment(EnvType.CLIENT)
public interface DebugRenderer_Duck {
WeepingSkyDebugRenderer crystalline_sky$getWeepingSkyDebugRenderer();
int crystalline_sky$toggleWeepingSky();
}
@@ -48,17 +48,17 @@ public class CrystallineBlocks {
return getSkyLightLevel(state) > 0; return getSkyLightLevel(state) > 0;
} }
public static boolean isWeepingSky(BlockState state) {
return state.isOf(WEEPING_SKY);
}
public static int getSkyLightLevel(BlockState state) { public static int getSkyLightLevel(BlockState state) {
if (state.isOf(SKY_LIGHT)) { if (state.isOf(SKY_LIGHT)) {
return state.get(LightBlock.LEVEL_15); return state.get(LightBlock.LEVEL_15);
} else if (state.isOf(SKY) || state.isOf(WEEPING_SKY)) { } else if (state.isOf(SKY)) { // weeping sky explicitly excluded, that's handled differently
return 15; return 15;
} }
return 0; return 0;
} }
public static boolean isWeepingSky(BlockState state) {
return state.isOf(WEEPING_SKY);
}
} }
@@ -0,0 +1,25 @@
package io.github.slimeistdev.crystalline_sky.registry;
import io.github.slimeistdev.crystalline_sky.CrystallineSky;
import net.minecraft.component.ComponentType;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Unit;
import java.util.function.UnaryOperator;
public class CrystallineDataComponentTypes {
public static final ComponentType<Unit> WEEPING_SKY_DEBUG_COLUMN_TOOL = register(
"weeping_sky_debug_column_tool",
b -> b
.packetCodec(Unit.PACKET_CODEC)
.codec(Unit.CODEC)
);
@SuppressWarnings("SameParameterValue")
private static <T> ComponentType<T> register(String id, UnaryOperator<ComponentType.Builder<T>> builderOperator) {
return Registry.register(Registries.DATA_COMPONENT_TYPE, CrystallineSky.id(id), builderOperator.apply(ComponentType.builder()).build());
}
public static void init() {}
}
@@ -1,5 +1,6 @@
package io.github.slimeistdev.crystalline_sky.registry; package io.github.slimeistdev.crystalline_sky.registry;
import io.github.slimeistdev.crystalline_sky.items.WeepingSkyBlockItem;
import net.minecraft.block.LightBlock; import net.minecraft.block.LightBlock;
import net.minecraft.component.DataComponentTypes; import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.BlockStateComponent; import net.minecraft.component.type.BlockStateComponent;
@@ -15,7 +16,7 @@ public class CrystallineItems {
public static final Item WEEPING_SKY = Items.register( public static final Item WEEPING_SKY = Items.register(
CrystallineBlocks.WEEPING_SKY, CrystallineBlocks.WEEPING_SKY,
settings -> settings.rarity(Rarity.EPIC) (block, settings) -> new WeepingSkyBlockItem(block, settings.rarity(Rarity.EPIC))
); );
public static final Item SKY_LIGHT = Items.register( public static final Item SKY_LIGHT = Items.register(
@@ -0,0 +1,47 @@
package io.github.slimeistdev.crystalline_sky.util;
public class ArrayUtils {
/**
* Binary search for the first index i in the sorted array where array[i] < value and array[i + 1] > value.
* @param array must be sorted in ascending order
* @param value the value to find a place for
* @return the index to insert at, or -1 if no such index exists
*/
public static int binarySearchSortedInsertionPoint(int[] array, int value) {
return binarySearchSortedInsertionPoint(array, value, array.length);
}
/**
* Binary search for the first index i in the sorted array where array[i] < value and array[i + 1] > value.
* @param array must be sorted in ascending order
* @param value the value to find a place for
* @param length limits the search to indexes [0, length)
* @return the index to insert at, or -1 if no such index exists
*/
public static int binarySearchSortedInsertionPoint(int[] array, int value, int length) {
if (length == 0) return 0;
if (value < array[0]) return 0;
if (value > array[length - 1]) return length;
int left = 0;
int right = length - 1;
while (left < right) {
int mid = (left + right) / 2;
int midValue = array[mid];
if (midValue == value)
return -1;
if (midValue < value) {
left = mid + 1;
} else { // midValue > value
right = mid;
}
}
if (array[left] == value) return -1;
if (array[right] == value) return -1;
return left;
}
}
@@ -1,5 +1,15 @@
{ {
"block.crystalline_sky.sky": "Sky", "block.crystalline_sky.sky": "Sky",
"block.crystalline_sky.weeping_sky": "Weeping Sky", "block.crystalline_sky.weeping_sky": "Weeping Sky",
"block.crystalline_sky.sky_light": "Sky Light" "block.crystalline_sky.sky_light": "Sky Light",
"crystalline_sky.key.toggle_weeping_sky_debug": "Toggle Weeping Sky Debug",
"crystalline_sky.category.crystalline_sky": "Crystalline Sky",
"message.crystalline_sky.weeping_sky_debug.column.cleared": "Cleared Weeping Sky debug column.",
"message.crystalline_sky.weeping_sky_debug.column.set": "Set Weeping Sky debug column to [%s, %s].",
"message.crystalline_sky.weeping_sky_debug.0": "Weeping Sky debug: off",
"message.crystalline_sky.weeping_sky_debug.1": "Weeping Sky debug: lit runs",
"message.crystalline_sky.weeping_sky_debug.2": "Weeping Sky debug: unlit runs"
} }
@@ -8,12 +8,15 @@
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "JAVA_21",
"mixins": [ "mixins": [
"ChunkLightProviderMixin", "ChunkLightProviderMixin",
"ChunkSkyLightMixin",
"ChunkSkyLightProviderMixin", "ChunkSkyLightProviderMixin",
"LightStorageAccessor" "LightStorageAccessor",
"SkyLightStorageAccessor"
], ],
"client": [ "client": [
"client.BlockRenderLayerMixin", "client.BlockRenderLayerMixin",
"client.ClientWorldMixin", "client.ClientWorldMixin",
"client.DebugRendererMixin",
"client.DefaultFramebufferSetMixin", "client.DefaultFramebufferSetMixin",
"client.FramebufferMixin", "client.FramebufferMixin",
"client.WorldRendererAccessor", "client.WorldRendererAccessor",