diff --git a/custom_mappings.tiny b/custom_mappings.tiny index 7ab4394..f13fee1 100644 --- a/custom_mappings.tiny +++ b/custom_mappings.tiny @@ -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 i field_44745 ADD_SKY_SOURCE_ENTRY f Ljb$a; j field_44746 scratchPos - m (IIII)V a method_51586 removeSourcesBelow m (JLjh;IZI)V a method_51587 propagateFromEmptySections - p 0 blockPos - p 1 direction - p 2 lightLevel - p 3 shouldIncrease - p 4 emptySections + p 1 blockPos + p 3 direction + p 4 lightLevel + p 5 shouldIncrease + p 6 emptySections 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 + 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 v 3 5 -1 _ _ x v 4 11 -1 _ _ y diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/CrystallineSkyClient.java b/src/main/java/io/github/slimeistdev/crystalline_sky/CrystallineSkyClient.java index e7fdbe1..f79e4d4 100644 --- a/src/main/java/io/github/slimeistdev/crystalline_sky/CrystallineSkyClient.java +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/CrystallineSkyClient.java @@ -1,14 +1,47 @@ package io.github.slimeistdev.crystalline_sky; 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 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.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 { @Override public void onInitializeClient() { BlockRenderLayerMap.putBlock(CrystallineBlocks.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); } } diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/ModSetup.java b/src/main/java/io/github/slimeistdev/crystalline_sky/ModSetup.java index e846e69..c004e04 100644 --- a/src/main/java/io/github/slimeistdev/crystalline_sky/ModSetup.java +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/ModSetup.java @@ -1,10 +1,12 @@ package io.github.slimeistdev.crystalline_sky; import io.github.slimeistdev.crystalline_sky.registry.CrystallineBlocks; +import io.github.slimeistdev.crystalline_sky.registry.CrystallineDataComponentTypes; import io.github.slimeistdev.crystalline_sky.registry.CrystallineItems; public class ModSetup { public static void init() { + CrystallineDataComponentTypes.init(); CrystallineBlocks.init(); CrystallineItems.init(); } diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/IntWeepingStorage.java b/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/IntWeepingStorage.java new file mode 100644 index 0000000..8aa64c2 --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/IntWeepingStorage.java @@ -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 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 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 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 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 stringConsumer) { + int index = getIndex(localX, localZ); + int size = sizes[index]; + int[] column = columns[index]; + + if (size == 0) { + stringConsumer.accept(" "); + } 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 wrapLitRuns(int splitsSize, int[] splits, Iterable iterable) { + if (splitsSize <= 0) return iterable; + return () -> new LitSplittingIterator(splitsSize, splits, iterable.iterator()); + } + + private class LitSplittingIterator implements Iterator { + private final int[] splits; + private final Iterator iterator; + + private final Deque remainingSplits = new ArrayDeque<>(); + + private int currentSplitIndex; + + private LitSplittingIterator(int splitsSize, int[] splits, Iterator 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 wrapUnlitRuns(int splitsSize, int[] splits, Iterable iterable) { + if (splitsSize <= 0) return iterable; + return () -> new UnlitSplittingIterator(splitsSize, splits, iterable.iterator()); + } + + private class UnlitSplittingIterator implements Iterator { + private final int[] splits; + private final Iterator iterator; + + private final Deque remainingSplits = new ArrayDeque<>(); + + private int currentSplitIndex; + + private UnlitSplittingIterator(int splitsSize, int[] splits, Iterator 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; + } + }*/ +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/MutableWeepingScanner.java b/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/MutableWeepingScanner.java new file mode 100644 index 0000000..fe58067 --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/MutableWeepingScanner.java @@ -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); + } + } +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/PeekableIntIterator.java b/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/PeekableIntIterator.java new file mode 100644 index 0000000..bebb7ba --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/PeekableIntIterator.java @@ -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(); + } +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/WeepingStorage.java b/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/WeepingStorage.java new file mode 100644 index 0000000..379a3a0 --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/WeepingStorage.java @@ -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 iterateLitRuns(int localX, int localZ, int lowestSourceY); + + Iterable 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 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); + } +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/client/WeepingSkyDebugRenderer.java b/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/client/WeepingSkyDebugRenderer.java new file mode 100644 index 0000000..571fe1d --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/infrastructure/client/WeepingSkyDebugRenderer.java @@ -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 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; + } +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/items/WeepingSkyBlockItem.java b/src/main/java/io/github/slimeistdev/crystalline_sky/items/WeepingSkyBlockItem.java new file mode 100644 index 0000000..494f9c2 --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/items/WeepingSkyBlockItem.java @@ -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); + } +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/items/WeepingSkyBlockItemClient.java b/src/main/java/io/github/slimeistdev/crystalline_sky/items/WeepingSkyBlockItemClient.java new file mode 100644 index 0000000..0b842bb --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/items/WeepingSkyBlockItemClient.java @@ -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); + } +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/ChunkLightProviderMixin.java b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/ChunkLightProviderMixin.java index cfe27ec..f0fa99b 100644 --- a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/ChunkLightProviderMixin.java +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/ChunkLightProviderMixin.java @@ -11,6 +11,8 @@ import org.spongepowered.asm.mixin.Mixin; public class ChunkLightProviderMixin { @WrapMethod(method = "needsLightUpdate") private static boolean updateSky(BlockState oldState, BlockState newState, Operation 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)); } } diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/ChunkSkyLightMixin.java b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/ChunkSkyLightMixin.java new file mode 100644 index 0000000..1bd1f9e --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/ChunkSkyLightMixin.java @@ -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 = "", 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 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); + } +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/ChunkSkyLightProviderMixin.java b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/ChunkSkyLightProviderMixin.java index 76e7a71..1d1525e 100644 --- a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/ChunkSkyLightProviderMixin.java +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/ChunkSkyLightProviderMixin.java @@ -1,14 +1,23 @@ 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 net.minecraft.block.BlockState; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.ChunkPos; import net.minecraft.util.math.ChunkSectionPos; +import net.minecraft.util.math.Direction; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.ChunkProvider; import net.minecraft.world.chunk.ChunkToNibbleArrayMap; 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.LightSourceView; 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.callback.CallbackInfo; +// todo replace all name = "..." locals with ordinal @Mixin(ChunkSkyLightProvider.class) public abstract class ChunkSkyLightProviderMixin, S extends LightStorage> extends ChunkLightProvider { @Shadow @Final 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) { super(chunkProvider, lightStorage); } @@ -32,12 +59,38 @@ public abstract class ChunkSkyLightProviderMixin 0 && ((LightStorageAccessor) lightStorage).crystalline_sky$callIsSectionInEnabledColumn(ChunkSectionPos.fromBlockPos(blockPos))) { 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 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")) private void propagateCrystallineSkyLight(ChunkPos chunkPos, CallbackInfo ci) { LightSourceView lightSourceView = this.chunkProvider.getChunk(chunkPos.x, chunkPos.z); @@ -47,4 +100,136 @@ public abstract class ChunkSkyLightProviderMixin= 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); + } } diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/LightStorageAccessor.java b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/LightStorageAccessor.java index 7e74e17..d7a9528 100644 --- a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/LightStorageAccessor.java +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/LightStorageAccessor.java @@ -8,4 +8,13 @@ import org.spongepowered.asm.mixin.gen.Invoker; public interface LightStorageAccessor { @Invoker("isSectionInEnabledColumn") 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); } diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/SkyLightStorageAccessor.java b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/SkyLightStorageAccessor.java new file mode 100644 index 0000000..b00e483 --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/SkyLightStorageAccessor.java @@ -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); +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/client/DebugRendererMixin.java b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/client/DebugRendererMixin.java new file mode 100644 index 0000000..e2b9d26 --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/client/DebugRendererMixin.java @@ -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 = "", 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(); + } +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/client/WorldRendererMixin.java b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/client/WorldRendererMixin.java index ccf4ba5..282a45c 100644 --- a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/client/WorldRendererMixin.java +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin/client/WorldRendererMixin.java @@ -158,7 +158,9 @@ public abstract class WorldRendererMixin { if (blockRenderLayer == BlockRenderLayerExt.CRYSTALLINE_SKY_SKY && client.player != null && (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 alpha = (MathHelper.sin(f / 10.0f) + 1.0f) / 2.0f; diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin_ducks/ChunkSkyLight_Duck.java b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin_ducks/ChunkSkyLight_Duck.java new file mode 100644 index 0000000..2cdd180 --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin_ducks/ChunkSkyLight_Duck.java @@ -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(); +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/mixin_ducks/client/DebugRenderer_Duck.java b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin_ducks/client/DebugRenderer_Duck.java new file mode 100644 index 0000000..685c689 --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/mixin_ducks/client/DebugRenderer_Duck.java @@ -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(); +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/registry/CrystallineBlocks.java b/src/main/java/io/github/slimeistdev/crystalline_sky/registry/CrystallineBlocks.java index 80f341c..4384bf6 100644 --- a/src/main/java/io/github/slimeistdev/crystalline_sky/registry/CrystallineBlocks.java +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/registry/CrystallineBlocks.java @@ -48,17 +48,17 @@ public class CrystallineBlocks { return getSkyLightLevel(state) > 0; } - public static boolean isWeepingSky(BlockState state) { - return state.isOf(WEEPING_SKY); - } - public static int getSkyLightLevel(BlockState state) { if (state.isOf(SKY_LIGHT)) { 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 0; } + + public static boolean isWeepingSky(BlockState state) { + return state.isOf(WEEPING_SKY); + } } diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/registry/CrystallineDataComponentTypes.java b/src/main/java/io/github/slimeistdev/crystalline_sky/registry/CrystallineDataComponentTypes.java new file mode 100644 index 0000000..8188a04 --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/registry/CrystallineDataComponentTypes.java @@ -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 WEEPING_SKY_DEBUG_COLUMN_TOOL = register( + "weeping_sky_debug_column_tool", + b -> b + .packetCodec(Unit.PACKET_CODEC) + .codec(Unit.CODEC) + ); + + @SuppressWarnings("SameParameterValue") + private static ComponentType register(String id, UnaryOperator> builderOperator) { + return Registry.register(Registries.DATA_COMPONENT_TYPE, CrystallineSky.id(id), builderOperator.apply(ComponentType.builder()).build()); + } + + public static void init() {} +} diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/registry/CrystallineItems.java b/src/main/java/io/github/slimeistdev/crystalline_sky/registry/CrystallineItems.java index 707f77b..573b288 100644 --- a/src/main/java/io/github/slimeistdev/crystalline_sky/registry/CrystallineItems.java +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/registry/CrystallineItems.java @@ -1,5 +1,6 @@ package io.github.slimeistdev.crystalline_sky.registry; +import io.github.slimeistdev.crystalline_sky.items.WeepingSkyBlockItem; import net.minecraft.block.LightBlock; import net.minecraft.component.DataComponentTypes; import net.minecraft.component.type.BlockStateComponent; @@ -15,7 +16,7 @@ public class CrystallineItems { public static final Item WEEPING_SKY = Items.register( 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( diff --git a/src/main/java/io/github/slimeistdev/crystalline_sky/util/ArrayUtils.java b/src/main/java/io/github/slimeistdev/crystalline_sky/util/ArrayUtils.java new file mode 100644 index 0000000..567d8cb --- /dev/null +++ b/src/main/java/io/github/slimeistdev/crystalline_sky/util/ArrayUtils.java @@ -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; + } +} diff --git a/src/main/resources/assets/crystalline_sky/lang/en_us.json b/src/main/resources/assets/crystalline_sky/lang/en_us.json index 87884a6..4cdfe40 100644 --- a/src/main/resources/assets/crystalline_sky/lang/en_us.json +++ b/src/main/resources/assets/crystalline_sky/lang/en_us.json @@ -1,5 +1,15 @@ { "block.crystalline_sky.sky": "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" } diff --git a/src/main/resources/crystalline_sky.mixins.json b/src/main/resources/crystalline_sky.mixins.json index 4c2ccd2..f90666c 100644 --- a/src/main/resources/crystalline_sky.mixins.json +++ b/src/main/resources/crystalline_sky.mixins.json @@ -8,12 +8,15 @@ "compatibilityLevel": "JAVA_21", "mixins": [ "ChunkLightProviderMixin", + "ChunkSkyLightMixin", "ChunkSkyLightProviderMixin", - "LightStorageAccessor" + "LightStorageAccessor", + "SkyLightStorageAccessor" ], "client": [ "client.BlockRenderLayerMixin", "client.ClientWorldMixin", + "client.DebugRendererMixin", "client.DefaultFramebufferSetMixin", "client.FramebufferMixin", "client.WorldRendererAccessor",