From eee7573d453582d71cad807340b5d535e91749e7 Mon Sep 17 00:00:00 2001 From: CursedFlames <18627001+CursedFlames@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:11:00 +1200 Subject: [PATCH 1/2] reimplement 3d loading screen for cubic worlds --- .../gui/render/CCNeoForgeRenderingHooks.java | 16 ++ .../client/gui/render/package-info.java | 7 + .../pip/WorldLoadingCubeStatusesRenderer.java | 233 ++++++++++++++++++ .../client/gui/render/pip/package-info.java | 7 + .../WorldLoadingCubeStatusesRenderState.java | 23 ++ .../gui/render/state/pip/package-info.java | 7 + .../gui/screens/CubicLevelLoadingScreen.java | 152 ++++++++++++ .../client/gui/screens/package-info.java | 7 + .../UserFunction.java | 100 ++++++++ .../package-info.java | 7 + .../gui/screens/MixinLevelLoadingScreen.java | 36 +++ .../core/client/gui/screens/package-info.java | 7 + .../MixinStoringChunkProgressListener.java | 17 +- .../progress/StoringCloProgressListener.java | 9 + 14 files changed, 627 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/CCNeoForgeRenderingHooks.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/package-info.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/pip/WorldLoadingCubeStatusesRenderer.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/pip/package-info.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/state/pip/WorldLoadingCubeStatusesRenderState.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/state/pip/package-info.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/CubicLevelLoadingScreen.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/package-info.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/figureoutwheretoputthislater/UserFunction.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/figureoutwheretoputthislater/package-info.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/client/gui/screens/MixinLevelLoadingScreen.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/client/gui/screens/package-info.java create mode 100644 src/main/java/io/github/opencubicchunks/cubicchunks/server/level/progress/StoringCloProgressListener.java diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/CCNeoForgeRenderingHooks.java b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/CCNeoForgeRenderingHooks.java new file mode 100644 index 00000000..4d1c7c6e --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/CCNeoForgeRenderingHooks.java @@ -0,0 +1,16 @@ +package io.github.opencubicchunks.cubicchunks.client.gui.render; + +import io.github.opencubicchunks.cubicchunks.client.gui.render.pip.WorldLoadingCubeStatusesRenderer; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.RegisterPictureInPictureRenderersEvent; + +@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD) +public class CCNeoForgeRenderingHooks { + private CCNeoForgeRenderingHooks() {} + + @SubscribeEvent + public static void registerPipRenderers(RegisterPictureInPictureRenderersEvent event) { + event.register(WorldLoadingCubeStatusesRenderer::new); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/package-info.java b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/package-info.java new file mode 100644 index 00000000..491db0e5 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package io.github.opencubicchunks.cubicchunks.client.gui.render; + +import javax.annotation.ParametersAreNonnullByDefault; + +import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/pip/WorldLoadingCubeStatusesRenderer.java b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/pip/WorldLoadingCubeStatusesRenderer.java new file mode 100644 index 00000000..499fa727 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/pip/WorldLoadingCubeStatusesRenderer.java @@ -0,0 +1,233 @@ +package io.github.opencubicchunks.cubicchunks.client.gui.render.pip; + +import java.util.EnumSet; + +import com.mojang.blaze3d.ProjectionType; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import io.github.opencubicchunks.cc_core.api.CubicConstants; +import io.github.opencubicchunks.cc_core.utils.Coords; +import io.github.opencubicchunks.cubicchunks.client.gui.render.state.pip.WorldLoadingCubeStatusesRenderState; +import io.github.opencubicchunks.cubicchunks.client.gui.screens.CubicLevelLoadingScreen; +import io.github.opencubicchunks.cubicchunks.server.level.progress.StoringCloProgressListener; +import net.minecraft.client.gui.render.pip.PictureInPictureRenderer; +import net.minecraft.client.renderer.CachedPerspectiveProjectionMatrixBuffer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.core.Direction; +import net.minecraft.server.level.progress.StoringChunkProgressListener; +import net.minecraft.util.ARGB; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.joml.Vector3f; + +/** + * Picture-in-Picture renderer used to render the 3d loading animation used when loading into a cubic world. + */ +public class WorldLoadingCubeStatusesRenderer extends PictureInPictureRenderer { + // "I want to essentially get rid of RenderType as a concept" - Dinnerbone + // so this will probably be obsolete soon + public static final RenderType WORLD_LOADING_CUBE_STATUSES = RenderType.create("cubicchunks_loading_cube_statuses", + RenderType.TRANSIENT_BUFFER_SIZE, false, true, RenderPipelines.GUI, RenderType.CompositeState.builder().createCompositeState(false)); + + // PictureInPictureRenderer defaults to an orthographic projection matrix, so we make our own perspective projection matrix + private final CachedPerspectiveProjectionMatrixBuffer perspectiveProjectionMatrixBuffer = new CachedPerspectiveProjectionMatrixBuffer( + "world loading cubes", 0.05F, 100F); + + private static final float FOV_DEGREES = 60F; + + /** Distance between the center of the loading animation and the camera */ + private static final float CAMERA_DISTANCE = 20F; + + /** Rotation of the loading animation about the X axis (rotating "downwards" towards the camera) */ + private static final float X_ROTATION_DEGREES = 30F; + + private static final double Y_ROTATION_DEGREES_PER_SECOND = 40; + + /** Constant controlling the size at which cubes are rendered */ + private static final float CUBE_BASE_SCALE = 0.12F; + + private static final float CHUNK_Y_OFFSET = -30F; + + private static final int CHUNK_ALPHA = 0x80; + + private static final int DARKEN_PERCENTAGE_CUBE_FRONT_BACK = 20; + private static final int DARKEN_PERCENTAGE_CUBE_SIDES = 30; + private static final int DARKEN_PERCENTAGE_CUBE_BOTTOM = 40; + + public WorldLoadingCubeStatusesRenderer(MultiBufferSource.BufferSource bufferSource) { + super(bufferSource); + } + + @Override public void close() { + super.close(); + + perspectiveProjectionMatrixBuffer.close(); + } + + @Override public Class getRenderStateClass() { + return WorldLoadingCubeStatusesRenderState.class; + } + + @SuppressWarnings("checkstyle:MagicNumber") // millis to seconds; degrees modulo 360 + private static float yRotationAngleDegrees(long milliseconds) { + return (float) ((milliseconds * (Y_ROTATION_DEGREES_PER_SECOND / 1000)) % 360); + } + + @Override protected void renderToTexture(WorldLoadingCubeStatusesRenderState state, PoseStack poseStackUnused) { + int width = state.x1() - state.x0(); + int height = state.y1() - state.y0(); + RenderSystem.setProjectionMatrix(perspectiveProjectionMatrixBuffer.getBuffer(width, height, FOV_DEGREES), ProjectionType.PERSPECTIVE); + + // The PoseStack passed to this method is set up for the orthographic projection matrix; + // as we're using perspective projection instead, we set up our own PoseStack. + var poseStack = new PoseStack(); + + poseStack.translate(0f, 0f, -CAMERA_DISTANCE); + poseStack.rotateAround(Axis.XP.rotationDegrees(X_ROTATION_DEGREES), 0.0F, 0.0F, 0.0F); + poseStack.rotateAround(Axis.YP.rotationDegrees(yRotationAngleDegrees(System.currentTimeMillis())), 0.0F, 0.0F, 0.0F); + var vertexConsumer = this.bufferSource.getBuffer(WORLD_LOADING_CUBE_STATUSES); + renderCubesAndChunks(vertexConsumer, poseStack, state.chunkProgressListener(), state.scale()); + this.bufferSource.endBatch(); + } + + @Override protected String getTextureLabel() { + return "world loading cube statuses"; + } + + private static void renderCubesAndChunks( + VertexConsumer vertexConsumer, PoseStack poseStack, StoringChunkProgressListener progressListener, float scale + ) { + int sectionRenderRadius = progressListener.getDiameter(); + drawChunks(vertexConsumer, poseStack, progressListener, scale, sectionRenderRadius); + drawCubes(vertexConsumer, poseStack, progressListener, scale, Coords.sectionToCubeCeil(sectionRenderRadius)); + + } + + private static void drawChunks( + VertexConsumer vertexConsumer, PoseStack poseStack, StoringChunkProgressListener progressListener, float scale, int sectionRenderRadius + ) { + for (int cdx = 0; cdx < sectionRenderRadius; cdx++) { + for (int cdz = 0; cdz < sectionRenderRadius; cdz++) { + ChunkStatus columnStatus = progressListener.getStatus(cdx, cdz); + if (columnStatus == null) { + continue; + } + int color = ARGB.color(CHUNK_ALPHA, + CubicLevelLoadingScreen.STATUS_COLORS.getOrDefault(columnStatus, CubicLevelLoadingScreen.DEFAULT_STATUS_COLOR)); + // We render the chunks underneath the cubes by rendering a smaller cube with only the top face visible + drawCube(vertexConsumer, poseStack, cdx - (float) sectionRenderRadius / 2, CHUNK_Y_OFFSET, cdz - (float) sectionRenderRadius / 2, + CUBE_BASE_SCALE * scale / CubicConstants.DIAMETER_IN_SECTIONS, color, EnumSet.of(Direction.UP)); + } + } + } + + private static void drawCubes( + VertexConsumer vertexConsumer, PoseStack poseStack, StoringChunkProgressListener progressListener, float scale, int cubeRenderRadius + ) { + EnumSet renderFaces = EnumSet.noneOf(Direction.class); + + var tracker = (StoringCloProgressListener) progressListener; + for (int dx = -1; dx <= cubeRenderRadius + 1; dx++) { + for (int dz = -1; dz <= cubeRenderRadius + 1; dz++) { + for (int dy = -1; dy <= cubeRenderRadius + 1; dy++) { + ChunkStatus status = tracker.cc_getStatus(dx, dy, dz); + if (status == null) { + continue; + } + renderFaces.clear(); + float alpha = CubicLevelLoadingScreen.STATUS_ALPHAS.getValue(status.getIndex()); + int color = ARGB.color(alpha, + CubicLevelLoadingScreen.STATUS_COLORS.getOrDefault(status, CubicLevelLoadingScreen.DEFAULT_STATUS_COLOR)); + for (Direction value : Direction.values()) { + ChunkStatus cubeStatus = tracker.cc_getStatus(dx + value.getStepX(), dy + value.getStepY(), dz + value.getStepZ()); + if (cubeStatus == null || !cubeStatus.isOrAfter(status)) { + renderFaces.add(value); + } + } + drawCube(vertexConsumer, poseStack, dx - (float) cubeRenderRadius / 2, dy - (float) cubeRenderRadius / 2, + dz - (float) cubeRenderRadius / 2, CUBE_BASE_SCALE * scale, color, renderFaces); + } + } + } + } + + private static void drawCube( + VertexConsumer vertexConsumer, PoseStack poseStack, float x, float y, float z, float scale, int color, EnumSet renderFaces + ) { + float x0 = x * scale; + float x1 = x0 + scale; + float y0 = y * scale; + float y1 = y0 + scale; + float z0 = z * scale; + float z1 = z0 + scale; + if (renderFaces.contains(Direction.UP)) { + // up face + vertex(vertexConsumer, poseStack, x0, y1, z0, 0, 1, 0, color); + vertex(vertexConsumer, poseStack, x0, y1, z1, 0, 1, 0, color); + vertex(vertexConsumer, poseStack, x1, y1, z1, 0, 1, 0, color); + vertex(vertexConsumer, poseStack, x1, y1, z0, 0, 1, 0, color); + } + if (renderFaces.contains(Direction.DOWN)) { + int c = darken(color, DARKEN_PERCENTAGE_CUBE_BOTTOM); + // down face + vertex(vertexConsumer, poseStack, x1, y0, z0, 0, -1, 0, c); + vertex(vertexConsumer, poseStack, x1, y0, z1, 0, -1, 0, c); + vertex(vertexConsumer, poseStack, x0, y0, z1, 0, -1, 0, c); + vertex(vertexConsumer, poseStack, x0, y0, z0, 0, -1, 0, c); + } + if (renderFaces.contains(Direction.EAST)) { + int c = darken(color, DARKEN_PERCENTAGE_CUBE_SIDES); + // right face + vertex(vertexConsumer, poseStack, x1, y1, z0, 1, 0, 0, c); + vertex(vertexConsumer, poseStack, x1, y1, z1, 1, 0, 0, c); + vertex(vertexConsumer, poseStack, x1, y0, z1, 1, 0, 0, c); + vertex(vertexConsumer, poseStack, x1, y0, z0, 1, 0, 0, c); + } + if (renderFaces.contains(Direction.WEST)) { + int c = darken(color, DARKEN_PERCENTAGE_CUBE_SIDES); + // left face + vertex(vertexConsumer, poseStack, x0, y0, z0, -1, 0, 0, c); + vertex(vertexConsumer, poseStack, x0, y0, z1, -1, 0, 0, c); + vertex(vertexConsumer, poseStack, x0, y1, z1, -1, 0, 0, c); + vertex(vertexConsumer, poseStack, x0, y1, z0, -1, 0, 0, c); + } + if (renderFaces.contains(Direction.NORTH)) { + int c = darken(color, DARKEN_PERCENTAGE_CUBE_FRONT_BACK); + // front face (facing camera) + vertex(vertexConsumer, poseStack, x0, y1, z0, 0, 0, -1, c); + vertex(vertexConsumer, poseStack, x1, y1, z0, 0, 0, -1, c); + vertex(vertexConsumer, poseStack, x1, y0, z0, 0, 0, -1, c); + vertex(vertexConsumer, poseStack, x0, y0, z0, 0, 0, -1, c); + } + if (renderFaces.contains(Direction.SOUTH)) { + int c = darken(color, DARKEN_PERCENTAGE_CUBE_FRONT_BACK); + // back face + vertex(vertexConsumer, poseStack, x0, y0, z1, 0, 0, 1, c); + vertex(vertexConsumer, poseStack, x1, y0, z1, 0, 0, 1, c); + vertex(vertexConsumer, poseStack, x1, y1, z1, 0, 0, 1, c); + vertex(vertexConsumer, poseStack, x0, y1, z1, 0, 0, 1, c); + } + } + + @SuppressWarnings("checkstyle:MagicNumber") // 100 for percentage + private static int darken(int color, int percentage) { + int r = ARGB.red(color); + r -= (r * percentage) / 100; + int g = ARGB.green(color); + g -= (g * percentage) / 100; + int b = ARGB.blue(color); + b -= (b * percentage) / 100; + return ARGB.color(ARGB.alpha(color), r, g, b); + } + + // TODO do we have any use for the normal vector? it's currently unused + private static void vertex(VertexConsumer vertexConsumer, PoseStack pose, float x, float y, float z, int nx, int ny, int nz, int color) { + var vec = new Vector3f(); + pose.last().pose().transformPosition(x, y, z, vec); + + vertexConsumer.addVertex(vec.x, vec.y, vec.z).setColor(color); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/pip/package-info.java b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/pip/package-info.java new file mode 100644 index 00000000..d4a6f5dd --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/pip/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package io.github.opencubicchunks.cubicchunks.client.gui.render.pip; + +import javax.annotation.ParametersAreNonnullByDefault; + +import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/state/pip/WorldLoadingCubeStatusesRenderState.java b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/state/pip/WorldLoadingCubeStatusesRenderState.java new file mode 100644 index 00000000..f231f011 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/state/pip/WorldLoadingCubeStatusesRenderState.java @@ -0,0 +1,23 @@ +package io.github.opencubicchunks.cubicchunks.client.gui.render.state.pip; + +import javax.annotation.Nullable; + +import io.github.opencubicchunks.cubicchunks.client.gui.render.pip.WorldLoadingCubeStatusesRenderer; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState; +import net.minecraft.server.level.progress.StoringChunkProgressListener; + +/** + * PiP render state for {@link WorldLoadingCubeStatusesRenderer}. + * Stores a reference to the {@link StoringChunkProgressListener} used for tracking world load progress. + */ +public record WorldLoadingCubeStatusesRenderState( + StoringChunkProgressListener chunkProgressListener, int x0, int y0, int x1, int y1, float scale, @Nullable ScreenRectangle scissorArea, + @Nullable ScreenRectangle bounds +) implements PictureInPictureRenderState { + public WorldLoadingCubeStatusesRenderState( + StoringChunkProgressListener chunkProgressListener, int x0, int y0, int x1, int y1, float scale, @Nullable ScreenRectangle scissorArea + ) { + this(chunkProgressListener, x0, y0, x1, y1, scale, scissorArea, PictureInPictureRenderState.getBounds(x0, y0, x1, y1, scissorArea)); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/state/pip/package-info.java b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/state/pip/package-info.java new file mode 100644 index 00000000..2af27a53 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/render/state/pip/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package io.github.opencubicchunks.cubicchunks.client.gui.render.state.pip; + +import javax.annotation.ParametersAreNonnullByDefault; + +import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/CubicLevelLoadingScreen.java b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/CubicLevelLoadingScreen.java new file mode 100644 index 00000000..529199af --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/CubicLevelLoadingScreen.java @@ -0,0 +1,152 @@ +package io.github.opencubicchunks.cubicchunks.client.gui.screens; + +import java.util.List; +import java.util.Locale; + +import io.github.opencubicchunks.cc_core.api.CubicConstants; +import io.github.opencubicchunks.cc_core.utils.Coords; +import io.github.opencubicchunks.cubicchunks.CubicChunks; +import io.github.opencubicchunks.cubicchunks.client.gui.render.pip.WorldLoadingCubeStatusesRenderer; +import io.github.opencubicchunks.cubicchunks.client.gui.render.state.pip.WorldLoadingCubeStatusesRenderState; +import io.github.opencubicchunks.cubicchunks.figureoutwheretoputthislater.UserFunction; +import io.github.opencubicchunks.cubicchunks.server.level.progress.StoringCloProgressListener; +import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.server.level.progress.StoringChunkProgressListener; +import net.minecraft.util.ARGB; +import net.minecraft.util.Mth; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +/** + * When loading a cubic world, we replace the chunk visualization of the vanilla loading screen with our own 3d visualization, + * and display a color key for the chunk status colors. + *
+ *
+ * See also: {@link WorldLoadingCubeStatusesRenderer}, the PiP renderer used to render the cube loading visualization. + */ +public class CubicLevelLoadingScreen { + private CubicLevelLoadingScreen() {} + + public static final Object2IntMap STATUS_COLORS = Object2IntMaps.unmodifiable(getStatusColors()); + + public static final int DEFAULT_STATUS_COLOR = 0xFF00FF; + + private static final int COLOR_KEY_SQUARE_SIZE = 11; + private static final int COLOR_KEY_MARGIN = 3; + + // Alpha values are arbitrary; current values were chosen for visibility of sections while loading + @SuppressWarnings("checkstyle:MagicNumber") + public static final UserFunction STATUS_ALPHAS = UserFunction.builder().point(0, 0.13f).point(ChunkStatus.STRUCTURE_STARTS.getIndex(), 0.26f) + .point(ChunkStatus.BIOMES.getIndex(), 0.37f).point(ChunkStatus.CARVERS.getIndex(), 0.74f).point(ChunkStatus.FEATURES.getIndex(), 0.3f) + .point(ChunkStatus.FULL.getIndex(), 1).build(); + + @SuppressWarnings({ "unused", "checkstyle:MagicNumber" }) // This method is just here so intellij shows the status colors when viewing this file + private static void unusedColors() { + //@formatter:off + /* MINECRAFT:EMPTY: */ new java.awt.Color(0xFF00FF); + /* MINECRAFT:STRUCTURE_STARTS: */ new java.awt.Color(0x444444); + /* MINECRAFT:STRUCTURE_REFERENCES: */ new java.awt.Color(0xFF0000); + /* MINECRAFT:BIOMES: */ new java.awt.Color(0xFFAA00); + /* MINECRAFT:NOISE: */ new java.awt.Color(0xA9FF00); + /* MINECRAFT:SURFACE: */ new java.awt.Color(0x00FF00); + /* MINECRAFT:CARVERS: */ new java.awt.Color(0x00FFAA); + /* MINECRAFT:FEATURES: */ new java.awt.Color(0x00A9FF); + /* MINECRAFT:INITIALIZE_LIGHT: */ new java.awt.Color(0x0000FF); + /* MINECRAFT:LIGHT: */ new java.awt.Color(0xAA00FF); + /* MINECRAFT:SPAWN: */ new java.awt.Color(0xFF00A9); + /* MINECRAFT:FULL: */ new java.awt.Color(0xFFFFFF); + //@formatter:on + } + + @SuppressWarnings("checkstyle:MagicNumber") // colors in here are somewhat arbitrary + public static Object2IntArrayMap getStatusColors() { + Object2IntArrayMap map = new Object2IntArrayMap<>(); + List statusList = ChunkStatus.getStatusList(); + + map.put(statusList.get(0), 0xFF00FF); + CubicChunks.LOGGER.debug(statusList.get(0) + ": 0x" + Integer.toHexString(0xFF00FF).toUpperCase(Locale.ROOT)); + map.put(statusList.get(1), 0x444444); + CubicChunks.LOGGER.debug(statusList.get(1) + ": 0x" + Integer.toHexString(0x444444).toUpperCase(Locale.ROOT)); + + for (int i = 2; i < statusList.size() - 1; i++) { + ChunkStatus chunkStatus = statusList.get(i); + int v = Mth.hsvToRgb((float) (i - 2) / (statusList.size() - 3), 1, 1); + map.put(chunkStatus, v); + CubicChunks.LOGGER.debug(chunkStatus + ": 0x" + Integer.toHexString(v).toUpperCase(Locale.ROOT)); + } + + map.put(statusList.get(statusList.size() - 1), 0xFFFFFF); + CubicChunks.LOGGER.debug(statusList.get(statusList.size() - 1) + ": 0x" + Integer.toHexString(0xFFFFFF).toUpperCase(Locale.ROOT)); + + return map; + } + + // Called from MixinLevelLoadingScreen when the chunk diagram would be rendered + public static void doRender(GuiGraphics guiGraphics, StoringChunkProgressListener progressListener) { + int statusCount = ChunkStatus.getStatusList().size(); + int[] chunkStatusCounts = new int[statusCount]; + int[] cubeStatusCounts = new int[statusCount]; + + int sectionRenderRadius = progressListener.getDiameter(); + countChunks(progressListener, chunkStatusCounts, sectionRenderRadius); + countCubes(progressListener, cubeStatusCounts, Coords.sectionToCubeCeil(sectionRenderRadius)); + + renderColorKey(guiGraphics, chunkStatusCounts, cubeStatusCounts); + guiGraphics.submitPictureInPictureRenderState(new WorldLoadingCubeStatusesRenderState(progressListener, 0, 0, guiGraphics.guiWidth(), + guiGraphics.guiHeight(), CubicConstants.DIAMETER_IN_SECTIONS, null)); + } + + private static void countChunks(StoringChunkProgressListener progressListener, int[] chunkStatusCounts, int sectionRenderRadius) { + for (int cdx = 0; cdx < sectionRenderRadius; cdx++) { + for (int cdz = 0; cdz < sectionRenderRadius; cdz++) { + ChunkStatus status = progressListener.getStatus(cdx, cdz); + if (status == null) { + continue; + } + chunkStatusCounts[status.getIndex()]++; + } + } + } + + private static void countCubes(StoringChunkProgressListener progressListener, int[] cubeStatusCounts, int cubeRenderRadius) { + var tracker = (StoringCloProgressListener) progressListener; + for (int dx = -1; dx <= cubeRenderRadius + 1; dx++) { + for (int dz = -1; dz <= cubeRenderRadius + 1; dz++) { + for (int dy = -1; dy <= cubeRenderRadius + 1; dy++) { + ChunkStatus status = tracker.cc_getStatus(dx, dy, dz); + if (status == null) { + continue; + } + cubeStatusCounts[status.getIndex()]++; + } + } + } + } + + private static void renderColorKey(GuiGraphics guiGraphics, int[] chunkStatusCounts, int[] cubeStatusCounts) { + var font = Minecraft.getInstance().font; + + int x = 1; + int y = 1; + + int radius = COLOR_KEY_SQUARE_SIZE; + int margin = COLOR_KEY_MARGIN; + + for (ChunkStatus status : ChunkStatus.getStatusList()) { + if (status == ChunkStatus.EMPTY) { + continue; + } + // TODO maybe don't show counts outside of dev? it's a lot of clutter and users probably don't need it + // - or just make it configurable. Maybe have an option to disable the color key entirely + var statusLabel = status.getName().substring("minecraft:".length()) + " (" + chunkStatusCounts[status.getIndex()] + ", " + + cubeStatusCounts[status.getIndex()] + ")"; + guiGraphics.drawString(font, statusLabel, x + radius + margin, y + 2, ARGB.white(1)); + int color = ARGB.color(STATUS_ALPHAS.getValue(status.getIndex()), STATUS_COLORS.getOrDefault(status, DEFAULT_STATUS_COLOR)); + guiGraphics.fill(x, y, x + radius, y + radius, color); + y += radius + margin; + } + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/package-info.java b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/package-info.java new file mode 100644 index 00000000..dda35430 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package io.github.opencubicchunks.cubicchunks.client.gui.screens; + +import javax.annotation.ParametersAreNonnullByDefault; + +import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/figureoutwheretoputthislater/UserFunction.java b/src/main/java/io/github/opencubicchunks/cubicchunks/figureoutwheretoputthislater/UserFunction.java new file mode 100644 index 00000000..8286b46a --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/figureoutwheretoputthislater/UserFunction.java @@ -0,0 +1,100 @@ +package io.github.opencubicchunks.cubicchunks.figureoutwheretoputthislater; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import io.github.opencubicchunks.cc_core.utils.MathUtil; + +// TODO move this to cc_core? +public class UserFunction { + public static final Codec CODEC = RecordCodecBuilder.create( + (instance) -> instance.group(Codec.list(Entry.CODEC).fieldOf("values").forGetter((UserFunction config) -> Arrays.asList(config.values))) + .apply(instance, UserFunction::new)); + + private final Entry[] values; + + public UserFunction(List entry) { + values = entry.toArray(new Entry[0]); + } + + public UserFunction(Map funcMap) { + values = funcMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(e -> new Entry(e.getKey(), e.getValue())).toArray(Entry[]::new); + } + + public float getValue(float y) { + if (values.length == 0) { + return 0; + } + if (values.length == 1) { + return values[0].v; + } + Entry e1 = values[0]; + Entry e2 = values[1]; + + // TODO: binary search? do we want to support functions complex enough for it to be needed? Will it improve performance? + for (int i = 2; i < values.length; i++) { + if (values[i - 1].y < y) { + e1 = e2; + e2 = values[i]; + } + } + float yFract = MathUtil.unlerp(y, e1.y, e2.y); + return MathUtil.lerp(yFract, e1.v, e2.v); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Map map = new HashMap<>(); + + public Builder point(float y, float v) { + this.map.put(y, v); + return this; + } + + public UserFunction build() { + return new UserFunction(this.map); + } + } + + public static class Entry { + public static final Codec CODEC = RecordCodecBuilder.create((instance) -> instance + .group(Codec.FLOAT.fieldOf("y").forGetter((Entry config) -> config.y), Codec.FLOAT.fieldOf("v").forGetter((Entry config) -> config.v)) + .apply(instance, Entry::new)); + public float y; + public float v; + + public Entry() {} + + public Entry(float key, float value) { + this.y = key; + this.v = value; + } + + @Override public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Entry entry = (Entry) o; + return Float.compare(entry.y, y) == 0; + } + + @Override public int hashCode() { + return Objects.hash(y); + } + + @Override public String toString() { + return "Entry{" + "y=" + y + ", v=" + v + '}'; + } + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/figureoutwheretoputthislater/package-info.java b/src/main/java/io/github/opencubicchunks/cubicchunks/figureoutwheretoputthislater/package-info.java new file mode 100644 index 00000000..824de49f --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/figureoutwheretoputthislater/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package io.github.opencubicchunks.cubicchunks.figureoutwheretoputthislater; + +import javax.annotation.ParametersAreNonnullByDefault; + +import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/client/gui/screens/MixinLevelLoadingScreen.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/client/gui/screens/MixinLevelLoadingScreen.java new file mode 100644 index 00000000..0f9f6a55 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/client/gui/screens/MixinLevelLoadingScreen.java @@ -0,0 +1,36 @@ +package io.github.opencubicchunks.cubicchunks.mixin.core.client.gui.screens; + +import io.github.opencubicchunks.cubicchunks.CanBeCubic; +import io.github.opencubicchunks.cubicchunks.client.gui.screens.CubicLevelLoadingScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.LevelLoadingScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.server.level.progress.StoringChunkProgressListener; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LevelLoadingScreen.class) +public abstract class MixinLevelLoadingScreen extends Screen { + protected MixinLevelLoadingScreen() { + super(null); + } + + @Inject(method = "renderChunks", at = @At("HEAD"), cancellable = true) + private static void cc_onRenderChunks( + GuiGraphics guiGraphics, StoringChunkProgressListener progressListener, int centerX, int centerY, int size, int spacing, CallbackInfo ci + ) { + // TODO probably should check if overworld will be cubic before `level` is instantiated, + // otherwise there's a flash of the vanilla loading screen before we start rendering the CC one + Level level = Minecraft.getInstance().getSingleplayerServer().overworld(); + if (level == null || !((CanBeCubic) level).cc_isCubic()) { + return; + } + + ci.cancel(); + CubicLevelLoadingScreen.doRender(guiGraphics, progressListener); + } +} diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/client/gui/screens/package-info.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/client/gui/screens/package-info.java new file mode 100644 index 00000000..c9fe6e87 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/client/gui/screens/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package io.github.opencubicchunks.cubicchunks.mixin.core.client.gui.screens; + +import javax.annotation.ParametersAreNonnullByDefault; + +import io.github.opencubicchunks.cc_core.annotation.MethodsReturnNonnullByDefault; diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/progress/MixinStoringChunkProgressListener.java b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/progress/MixinStoringChunkProgressListener.java index 35197703..6c4de990 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/progress/MixinStoringChunkProgressListener.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/mixin/core/common/server/level/progress/MixinStoringChunkProgressListener.java @@ -9,18 +9,26 @@ import io.github.notstirred.dasm.api.annotations.selector.MethodSig; import io.github.notstirred.dasm.api.annotations.selector.Ref; import io.github.notstirred.dasm.api.annotations.transform.TransformFromMethod; +import io.github.opencubicchunks.cc_core.api.CubePos; +import io.github.opencubicchunks.cc_core.utils.Coords; import io.github.opencubicchunks.cc_core.world.level.CloPos; import io.github.opencubicchunks.cubicchunks.mixin.dasmsets.ChunkToCloSet; import io.github.opencubicchunks.cubicchunks.mixin.dasmsets.GlobalSet; import io.github.opencubicchunks.cubicchunks.server.level.progress.CloProgressListener; +import io.github.opencubicchunks.cubicchunks.server.level.progress.StoringCloProgressListener; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import net.minecraft.server.level.progress.StoringChunkProgressListener; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; @Dasm(value = ChunkToCloSet.class, target = @Ref(StoringChunkProgressListener.class)) @Mixin(StoringChunkProgressListener.class) -public abstract class MixinStoringChunkProgressListener implements CloProgressListener { +public abstract class MixinStoringChunkProgressListener implements CloProgressListener, StoringCloProgressListener { + @Shadow @Final private Long2ObjectOpenHashMap statuses; + @Shadow @Final private int radius; @AddFieldToSets(containers = GlobalSet.StoringChunkProgressListener_redirects.class, field = @FieldSig(type = @Ref(ChunkPos.class), name = "spawnPos")) private CloPos cc_spawnPos; @@ -31,4 +39,11 @@ public abstract class MixinStoringChunkProgressListener implements CloProgressLi @AddTransformToSets(ChunkToCloSet.StoringChunkProgressListener_redirects.class) @TransformFromMethod(@MethodSig("onStatusChange(Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/status/ChunkStatus;)V")) @Override public native void cc_onStatusChange(CloPos chunkPosition, @Nullable ChunkStatus newStatus); + + @Override @Nullable public ChunkStatus cc_getStatus(int cubeX, int cubeY, int cubeZ) { + int cubeRadius = Coords.sectionToCubeCeil(radius); + var spawnPos = cc_spawnPos != null && cc_spawnPos.isCube() ? cc_spawnPos.cubePos() : CubePos.ZERO; + return this.statuses.get( + CubePos.asLong(cubeX + spawnPos.getX() - cubeRadius, cubeY + spawnPos.getY() - cubeRadius, cubeZ + spawnPos.getZ() - cubeRadius)); + } } diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/server/level/progress/StoringCloProgressListener.java b/src/main/java/io/github/opencubicchunks/cubicchunks/server/level/progress/StoringCloProgressListener.java new file mode 100644 index 00000000..11fb0649 --- /dev/null +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/server/level/progress/StoringCloProgressListener.java @@ -0,0 +1,9 @@ +package io.github.opencubicchunks.cubicchunks.server.level.progress; + +import javax.annotation.Nullable; + +import net.minecraft.world.level.chunk.status.ChunkStatus; + +public interface StoringCloProgressListener { + @Nullable ChunkStatus cc_getStatus(int cubeX, int cubeY, int cubeZ); +} From df76166081561ae06d66481c150de66fadc7dc58 Mon Sep 17 00:00:00 2001 From: Tom Martin Date: Fri, 20 Jun 2025 12:09:50 +0100 Subject: [PATCH 2/2] Include full alpha color as a border to loadscreen key --- .../client/gui/screens/CubicLevelLoadingScreen.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/CubicLevelLoadingScreen.java b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/CubicLevelLoadingScreen.java index 529199af..1278ff5a 100644 --- a/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/CubicLevelLoadingScreen.java +++ b/src/main/java/io/github/opencubicchunks/cubicchunks/client/gui/screens/CubicLevelLoadingScreen.java @@ -144,8 +144,11 @@ private static void renderColorKey(GuiGraphics guiGraphics, int[] chunkStatusCou var statusLabel = status.getName().substring("minecraft:".length()) + " (" + chunkStatusCounts[status.getIndex()] + ", " + cubeStatusCounts[status.getIndex()] + ")"; guiGraphics.drawString(font, statusLabel, x + radius + margin, y + 2, ARGB.white(1)); - int color = ARGB.color(STATUS_ALPHAS.getValue(status.getIndex()), STATUS_COLORS.getOrDefault(status, DEFAULT_STATUS_COLOR)); - guiGraphics.fill(x, y, x + radius, y + radius, color); + int color = ARGB.opaque(STATUS_COLORS.getOrDefault(status, DEFAULT_STATUS_COLOR)); + int colorWithAlpha = ARGB.color(STATUS_ALPHAS.getValue(status.getIndex()), color); + guiGraphics.fill(x, y, x + radius, y + radius, colorWithAlpha); + guiGraphics.renderOutline(x, y, radius, radius, color); + y += radius + margin; } }