diff options
| author | Yasin <a.piri@hotmail.de> | 2023-10-09 12:58:02 +0200 |
|---|---|---|
| committer | Yasin <a.piri@hotmail.de> | 2023-10-09 12:58:02 +0200 |
| commit | bd3f0329d0e391bd84b5f9e3ff207d9dd9815853 (patch) | |
| tree | 2fd1d1ef625f57acc2e4916c967d8d2393844798 /src/main/java/de/hysky/skyblocker/utils/render | |
| parent | 2315b90da8117f28f66348927afdb621ee4fc815 (diff) | |
| download | Skyblocker-bd3f0329d0e391bd84b5f9e3ff207d9dd9815853.tar.gz Skyblocker-bd3f0329d0e391bd84b5f9e3ff207d9dd9815853.tar.bz2 Skyblocker-bd3f0329d0e391bd84b5f9e3ff207d9dd9815853.zip | |
new pr because fixing merge conflict would take too long
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/utils/render')
11 files changed, 938 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/FrustumUtils.java b/src/main/java/de/hysky/skyblocker/utils/render/FrustumUtils.java new file mode 100644 index 00000000..3fe79e43 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/FrustumUtils.java @@ -0,0 +1,21 @@ +package de.hysky.skyblocker.utils.render; + +import de.hysky.skyblocker.mixin.accessor.FrustumInvoker; +import de.hysky.skyblocker.mixin.accessor.WorldRendererAccessor; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.Frustum; +import net.minecraft.util.math.Box; + +public class FrustumUtils { + public static Frustum getFrustum() { + return ((WorldRendererAccessor) MinecraftClient.getInstance().worldRenderer).getFrustum(); + } + + public static boolean isVisible(Box box) { + return getFrustum().isVisible(box); + } + + public static boolean isVisible(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return ((FrustumInvoker) getFrustum()).invokeIsVisible(minX, minY, minZ, maxX, maxY, maxZ); + } +}
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java new file mode 100644 index 00000000..4630149c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java @@ -0,0 +1,247 @@ +package de.hysky.skyblocker.utils.render; + +import com.mojang.blaze3d.platform.GlStateManager.DstFactor; +import com.mojang.blaze3d.platform.GlStateManager.SrcFactor; +import com.mojang.blaze3d.systems.RenderSystem; +import de.hysky.skyblocker.mixin.accessor.BeaconBlockEntityRendererInvoker; +import me.x150.renderer.render.Renderer3d; +import de.hysky.skyblocker.utils.render.culling.OcclusionCulling; +import de.hysky.skyblocker.utils.render.title.Title; +import de.hysky.skyblocker.utils.render.title.TitleContainer; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.render.*; +import net.minecraft.client.render.VertexFormat.DrawMode; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.lwjgl.opengl.GL11; + +import java.awt.*; + +public class RenderHelper { + private static final Vec3d ONE = new Vec3d(1, 1, 1); + private static final int MAX_OVERWORLD_BUILD_HEIGHT = 319; + private static final MinecraftClient client = MinecraftClient.getInstance(); + + public static void renderFilledThroughWallsWithBeaconBeam(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) { + renderFilledThroughWalls(context, pos, colorComponents, alpha); + renderBeaconBeam(context, pos, colorComponents); + } + + public static void renderFilledThroughWalls(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) { + if (FrustumUtils.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1)) { + Renderer3d.renderThroughWalls(); + renderFilled(context, pos, colorComponents, alpha); + Renderer3d.stopRenderThroughWalls(); + } + } + + public static void renderFilledIfVisible(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) { + if (OcclusionCulling.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1)) { + renderFilled(context, pos, colorComponents, alpha); + } + } + + private static void renderFilled(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) { + Renderer3d.renderFilled(context.matrixStack(), new Color(colorComponents[0], colorComponents[1], colorComponents[2], alpha), Vec3d.of(pos), ONE); + } + + private static void renderBeaconBeam(WorldRenderContext context, BlockPos pos, float[] colorComponents) { + if (FrustumUtils.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, MAX_OVERWORLD_BUILD_HEIGHT, pos.getZ() + 1)) { + MatrixStack matrices = context.matrixStack(); + Vec3d camera = context.camera().getPos(); + + matrices.push(); + matrices.translate(pos.getX() - camera.getX(), pos.getY() - camera.getY(), pos.getZ() - camera.getZ()); + + Tessellator tessellator = RenderSystem.renderThreadTesselator(); + BufferBuilder buffer = tessellator.getBuffer(); + VertexConsumerProvider.Immediate consumer = VertexConsumerProvider.immediate(buffer); + + BeaconBlockEntityRendererInvoker.renderBeam(matrices, consumer, context.tickDelta(), context.world().getTime(), 0, MAX_OVERWORLD_BUILD_HEIGHT, colorComponents); + + consumer.draw(); + matrices.pop(); + } + } + + /** + * Renders the outline of a box with the specified color components and line width. + * This does not use renderer since renderer draws outline using debug lines with a fixed width. + */ + public static void renderOutline(WorldRenderContext context, Box box, float[] colorComponents, float lineWidth) { + if (FrustumUtils.isVisible(box)) { + MatrixStack matrices = context.matrixStack(); + Vec3d camera = context.camera().getPos(); + Tessellator tessellator = RenderSystem.renderThreadTesselator(); + BufferBuilder buffer = tessellator.getBuffer(); + + RenderSystem.setShader(GameRenderer::getRenderTypeLinesProgram); + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + RenderSystem.lineWidth(lineWidth); + RenderSystem.disableCull(); + RenderSystem.enableDepthTest(); + + matrices.push(); + matrices.translate(-camera.getX(), -camera.getY(), -camera.getZ()); + + buffer.begin(DrawMode.LINES, VertexFormats.LINES); + WorldRenderer.drawBox(matrices, buffer, box, colorComponents[0], colorComponents[1], colorComponents[2], 1f); + tessellator.draw(); + + matrices.pop(); + RenderSystem.lineWidth(1f); + RenderSystem.enableCull(); + RenderSystem.disableDepthTest(); + } + } + + /** + * Draws lines from point to point.<br><br> + * <p> + * Tip: To draw lines from the center of a block, offset the X, Y and Z each by 0.5 + * + * @param context The WorldRenderContext which supplies the matrices and tick delta + * @param points The points from which to draw lines between + * @param colorComponents An array of R, G and B color components + * @param alpha The alpha of the lines + * @param lineWidth The width of the lines + */ + public static void renderLinesFromPoints(WorldRenderContext context, Vec3d[] points, float[] colorComponents, float alpha, float lineWidth) { + Vec3d camera = context.camera().getPos(); + MatrixStack matrices = context.matrixStack(); + + matrices.push(); + matrices.translate(-camera.x, -camera.y, -camera.z); + + Tessellator tessellator = RenderSystem.renderThreadTesselator(); + BufferBuilder buffer = tessellator.getBuffer(); + Matrix4f projectionMatrix = matrices.peek().getPositionMatrix(); + Matrix3f normalMatrix = matrices.peek().getNormalMatrix(); + + GL11.glEnable(GL11.GL_LINE_SMOOTH); + GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST); + + RenderSystem.setShader(GameRenderer::getRenderTypeLinesProgram); + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + RenderSystem.lineWidth(lineWidth); + RenderSystem.enableBlend(); + RenderSystem.blendFunc(SrcFactor.SRC_ALPHA, DstFactor.ONE_MINUS_SRC_ALPHA); + RenderSystem.disableCull(); + RenderSystem.enableDepthTest(); + + buffer.begin(DrawMode.LINE_STRIP, VertexFormats.LINES); + + for (int i = 0; i < points.length; i++) { + Vec3d point = points[i]; + Vec3d nextPoint = (i + 1 == points.length) ? points[i - 1] : points[i + 1]; + Vector3f normalVec = new Vector3f((float) nextPoint.getX(), (float) nextPoint.getY(), (float) nextPoint.getZ()).sub((float) point.getX(), (float) point.getY(), (float) point.getZ()).normalize(); + + buffer + .vertex(projectionMatrix, (float) point.getX(), (float) point.getY(), (float) point.getZ()) + .color(colorComponents[0], colorComponents[1], colorComponents[2], alpha) + .normal(normalMatrix, normalVec.x, normalVec.y, normalVec.z) + .next(); + } + + tessellator.draw(); + + matrices.pop(); + GL11.glDisable(GL11.GL_LINE_SMOOTH); + RenderSystem.lineWidth(1f); + RenderSystem.disableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableCull(); + RenderSystem.disableDepthTest(); + } + + public static void renderText(WorldRenderContext context, Text text, Vec3d pos, boolean seeThrough) { + renderText(context, text, pos, 1, seeThrough); + } + + public static void renderText(WorldRenderContext context, Text text, Vec3d pos, float scale, boolean seeThrough) { + renderText(context, text, pos, scale, 0, seeThrough); + } + + public static void renderText(WorldRenderContext context, Text text, Vec3d pos, float scale, float yOffset, boolean seeThrough) { + renderText(context, text.asOrderedText(), pos, scale, yOffset, seeThrough); + } + + /** + * Renders text in the world space. + * + * @param seeThrough Whether the text should be able to be seen through walls or not. + */ + public static void renderText(WorldRenderContext context, OrderedText text, Vec3d pos, float scale, float yOffset, boolean seeThrough) { + MatrixStack matrices = context.matrixStack(); + Vec3d camera = context.camera().getPos(); + TextRenderer textRenderer = client.textRenderer; + + scale *= 0.025f; + + matrices.push(); + matrices.translate(pos.getX() - camera.getX(), pos.getY() - camera.getY(), pos.getZ() - camera.getZ()); + matrices.peek().getPositionMatrix().mul(RenderSystem.getModelViewMatrix()); + matrices.multiply(context.camera().getRotation()); + matrices.scale(-scale, -scale, scale); + + Matrix4f positionMatrix = matrices.peek().getPositionMatrix(); + float xOffset = -textRenderer.getWidth(text) / 2f; + + Tessellator tessellator = RenderSystem.renderThreadTesselator(); + BufferBuilder buffer = tessellator.getBuffer(); + VertexConsumerProvider.Immediate consumers = VertexConsumerProvider.immediate(buffer); + + RenderSystem.depthFunc(seeThrough ? GL11.GL_ALWAYS : GL11.GL_LEQUAL); + + textRenderer.draw(text, xOffset, yOffset, 0xFFFFFFFF, false, positionMatrix, consumers, TextRenderer.TextLayerType.SEE_THROUGH, 0, LightmapTextureManager.MAX_LIGHT_COORDINATE); + consumers.draw(); + + RenderSystem.depthFunc(GL11.GL_LEQUAL); + matrices.pop(); + } + + /** + * Adds the title to {@link TitleContainer} and {@link #playNotificationSound() plays the notification sound} if the title is not in the {@link TitleContainer} already. + * No checking needs to be done on whether the title is in the {@link TitleContainer} already by the caller. + * + * @param title the title + */ + public static void displayInTitleContainerAndPlaySound(Title title) { + if (TitleContainer.addTitle(title)) { + playNotificationSound(); + } + } + + /** + * Adds the title to {@link TitleContainer} for a set number of ticks and {@link #playNotificationSound() plays the notification sound} if the title is not in the {@link TitleContainer} already. + * No checking needs to be done on whether the title is in the {@link TitleContainer} already by the caller. + * + * @param title the title + * @param ticks the number of ticks the title will remain + */ + public static void displayInTitleContainerAndPlaySound(Title title, int ticks) { + if (TitleContainer.addTitle(title, ticks)) { + playNotificationSound(); + } + } + + private static void playNotificationSound() { + if (MinecraftClient.getInstance().player != null) { + MinecraftClient.getInstance().player.playSound(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP, 100f, 0.1f); + } + } + + public static boolean pointIsInArea(double x, double y, double x1, double y1, double x2, double y2) { + return x >= x1 && x <= x2 && y >= y1 && y <= y2; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java new file mode 100644 index 00000000..5f8d1592 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java @@ -0,0 +1,47 @@ +package de.hysky.skyblocker.utils.render.culling; + +import com.logisticscraft.occlusionculling.OcclusionCullingInstance; +import com.logisticscraft.occlusionculling.cache.ArrayOcclusionCache; +import com.logisticscraft.occlusionculling.util.Vec3d; +import de.hysky.skyblocker.utils.render.FrustumUtils; +import net.minecraft.client.MinecraftClient; + +public class OcclusionCulling { + private static final int TRACING_DISTANCE = 128; + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static OcclusionCullingInstance instance = null; + + // Reused objects to reduce allocation overhead + private static final Vec3d cameraPos = new Vec3d(0, 0, 0); + private static final Vec3d min = new Vec3d(0, 0, 0); + private static final Vec3d max = new Vec3d(0, 0, 0); + + /** + * Initializes the occlusion culling instance + */ + public static void init() { + instance = new OcclusionCullingInstance(TRACING_DISTANCE, new WorldProvider(), new ArrayOcclusionCache(TRACING_DISTANCE), 2); + } + + private static void updateCameraPos() { + var camera = CLIENT.gameRenderer.getCamera().getPos(); + cameraPos.set(camera.x, camera.y, camera.z); + } + + /** + * This first checks checks if the bounding box is within the camera's FOV, if + * it is then it checks for whether it's occluded or not. + * + * @return A boolean representing whether the bounding box is fully visible or + * not. + */ + public static boolean isVisible(double x1, double y1, double z1, double x2, double y2, double z2) { + if (!FrustumUtils.isVisible(x1, y1, z1, x2, y2, z2)) return false; + + updateCameraPos(); + min.set(x1, y1, z1); + max.set(x2, y2, z2); + + return instance.isAABBVisible(min, max, cameraPos); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java new file mode 100644 index 00000000..7ee0f0ed --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java @@ -0,0 +1,28 @@ +package de.hysky.skyblocker.utils.render.culling; + +import com.logisticscraft.occlusionculling.DataProvider; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.BlockPos; + +public class WorldProvider implements DataProvider { + private final static MinecraftClient CLIENT = MinecraftClient.getInstance(); + private ClientWorld world = null; + + @Override + public boolean prepareChunk(int chunkX, int chunkZ) { + this.world = CLIENT.world; + return this.world != null; + } + + @Override + public boolean isOpaqueFullCube(int x, int y, int z) { + BlockPos pos = new BlockPos(x, y, z); + return this.world.getBlockState(pos).isOpaqueFullCube(this.world, pos); + } + + @Override + public void cleanup() { + this.world = null; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/package-info.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/package-info.java new file mode 100644 index 00000000..1d5cdf98 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/package-info.java @@ -0,0 +1,4 @@ +/** + * Package dedicated to occlusion culling utilities + */ +package de.hysky.skyblocker.utils.render.culling;
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorHighlight.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorHighlight.java new file mode 100644 index 00000000..5451e1a8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorHighlight.java @@ -0,0 +1,24 @@ +package de.hysky.skyblocker.utils.render.gui; + +public record ColorHighlight(int slot, int color) { + private static final int RED_HIGHLIGHT = 64 << 24 | 255 << 16; + private static final int YELLOW_HIGHLIGHT = 128 << 24 | 255 << 16 | 255 << 8; + private static final int GREEN_HIGHLIGHT = 128 << 24 | 64 << 16 | 196 << 8 | 64; + private static final int GRAY_HIGHLIGHT = 128 << 24 | 64 << 16 | 64 << 8 | 64; + + public static ColorHighlight red(int slot) { + return new ColorHighlight(slot, RED_HIGHLIGHT); + } + + public static ColorHighlight yellow(int slot) { + return new ColorHighlight(slot, YELLOW_HIGHLIGHT); + } + + public static ColorHighlight green(int slot) { + return new ColorHighlight(slot, GREEN_HIGHLIGHT); + } + + public static ColorHighlight gray(int slot) { + return new ColorHighlight(slot, GRAY_HIGHLIGHT); + } +}
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java new file mode 100644 index 00000000..cf67e84c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java @@ -0,0 +1,44 @@ +package de.hysky.skyblocker.utils.render.gui; + +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; +import net.minecraft.item.ItemStack; + +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Abstract class for gui solvers. Extend this class to add a new gui solver, like terminal solvers or experiment solvers. + */ +public abstract class ContainerSolver { + private final Pattern containerName; + + protected ContainerSolver(String containerName) { + this.containerName = Pattern.compile(containerName); + } + + protected abstract boolean isEnabled(); + + public Pattern getName() { + return containerName; + } + + protected void start(GenericContainerScreen screen) { + } + + protected void reset() { + } + + protected abstract List<ColorHighlight> getColors(String[] groups, Map<Integer, ItemStack> slots); + + protected void trimEdges(Map<Integer, ItemStack> slots, int rows) { + for (int i = 0; i < rows; i++) { + slots.remove(9 * i); + slots.remove(9 * i + 8); + } + for (int i = 1; i < 8; i++) { + slots.remove(i); + slots.remove((rows - 1) * 9 + i); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java new file mode 100644 index 00000000..60e2b4e4 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java @@ -0,0 +1,125 @@ +package de.hysky.skyblocker.utils.render.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import de.hysky.skyblocker.mixin.accessor.HandledScreenAccessor; +import de.hysky.skyblocker.skyblock.dungeon.CroesusHelper; +import de.hysky.skyblocker.skyblock.dungeon.terminal.ColorTerminal; +import de.hysky.skyblocker.skyblock.dungeon.terminal.OrderTerminal; +import de.hysky.skyblocker.skyblock.dungeon.terminal.StartsWithTerminal; +import de.hysky.skyblocker.skyblock.experiment.ChronomatronSolver; +import de.hysky.skyblocker.skyblock.experiment.SuperpairsSolver; +import de.hysky.skyblocker.skyblock.experiment.UltrasequencerSolver; +import de.hysky.skyblocker.utils.Utils; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Manager class for {@link ContainerSolver}s like terminal solvers and experiment solvers. To add a new gui solver, extend {@link ContainerSolver} and register it in {@link #ContainerSolverManager()}. + */ +public class ContainerSolverManager { + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile(""); + private final ContainerSolver[] solvers; + private ContainerSolver currentSolver = null; + private String[] groups; + private List<ColorHighlight> highlights; + + public ContainerSolverManager() { + solvers = new ContainerSolver[]{ + new ColorTerminal(), + new OrderTerminal(), + new StartsWithTerminal(), + new CroesusHelper(), + new ChronomatronSolver(), + new SuperpairsSolver(), + new UltrasequencerSolver() + }; + } + + public ContainerSolver getCurrentSolver() { + return currentSolver; + } + + public void init() { + ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> { + if (Utils.isOnSkyblock() && screen instanceof GenericContainerScreen genericContainerScreen) { + ScreenEvents.afterRender(screen).register((screen1, context, mouseX, mouseY, delta) -> { + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.translate(((HandledScreenAccessor) genericContainerScreen).getX(), ((HandledScreenAccessor) genericContainerScreen).getY(), 300); + onDraw(context, genericContainerScreen.getScreenHandler().slots.subList(0, genericContainerScreen.getScreenHandler().getRows() * 9)); + matrices.pop(); + }); + ScreenEvents.remove(screen).register(screen1 -> clearScreen()); + onSetScreen(genericContainerScreen); + } else { + clearScreen(); + } + }); + } + + public void onSetScreen(@NotNull GenericContainerScreen screen) { + String screenName = screen.getTitle().getString(); + Matcher matcher = PLACEHOLDER_PATTERN.matcher(screenName); + for (ContainerSolver solver : solvers) { + if (solver.isEnabled()) { + matcher.usePattern(solver.getName()); + matcher.reset(); + if (matcher.matches()) { + currentSolver = solver; + groups = new String[matcher.groupCount()]; + for (int i = 0; i < groups.length; i++) { + groups[i] = matcher.group(i + 1); + } + currentSolver.start(screen); + return; + } + } + } + clearScreen(); + } + + public void clearScreen() { + if (currentSolver != null) { + currentSolver.reset(); + currentSolver = null; + } + } + + public void markDirty() { + highlights = null; + } + + public void onDraw(DrawContext context, List<Slot> slots) { + if (currentSolver == null) + return; + if (highlights == null) + highlights = currentSolver.getColors(groups, slotMap(slots)); + RenderSystem.enableDepthTest(); + RenderSystem.colorMask(true, true, true, false); + for (ColorHighlight highlight : highlights) { + Slot slot = slots.get(highlight.slot()); + int color = highlight.color(); + context.fillGradient(slot.x, slot.y, slot.x + 16, slot.y + 16, color, color); + } + RenderSystem.colorMask(true, true, true, true); + } + + private Map<Integer, ItemStack> slotMap(List<Slot> slots) { + Map<Integer, ItemStack> slotMap = new TreeMap<>(); + for (int i = 0; i < slots.size(); i++) { + slotMap.put(i, slots.get(i).getStack()); + } + return slotMap; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/title/Title.java b/src/main/java/de/hysky/skyblocker/utils/render/title/Title.java new file mode 100644 index 00000000..1e167afa --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/title/Title.java @@ -0,0 +1,53 @@ +package de.hysky.skyblocker.utils.render.title; + +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +/** + * Represents a title used for {@link TitleContainer}. + * + * @see TitleContainer + */ +public class Title { + private MutableText text; + protected float x = -1; + protected float y = -1; + + /** + * Constructs a new title with the given translation key and formatting to be applied. + * + * @param textKey the translation key + * @param formatting the formatting to be applied to the text + */ + public Title(String textKey, Formatting formatting) { + this(Text.translatable(textKey).formatted(formatting)); + } + + /** + * Constructs a new title with the given {@link MutableText}. + * Use {@link Text#literal(String)} or {@link Text#translatable(String)} to create a {@link MutableText} + * + * @param text the mutable text + */ + public Title(MutableText text) { + this.text = text; + } + + public MutableText getText() { + return text; + } + + public void setText(MutableText text) { + this.text = text; + } + + protected boolean isDefaultPos() { + return x == -1 && y == -1; + } + + protected void resetPos() { + this.x = -1; + this.y = -1; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java b/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java new file mode 100644 index 00000000..487e3d8b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java @@ -0,0 +1,175 @@ +package de.hysky.skyblocker.utils.render.title; + +import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.math.MathHelper; + +import java.util.LinkedHashSet; +import java.util.Set; + +public class TitleContainer { + /** + * The set of titles which will be rendered. + * + * @see #containsTitle(Title) + * @see #addTitle(Title) + * @see #addTitle(Title, int) + * @see #removeTitle(Title) + */ + private static final Set<Title> titles = new LinkedHashSet<>(); + + public static void init() { + HudRenderCallback.EVENT.register(TitleContainer::render); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("skyblocker") + .then(ClientCommandManager.literal("hud") + .then(ClientCommandManager.literal("titleContainer") + .executes(Scheduler.queueOpenScreenCommand(TitleContainerConfigScreen::new)))))); + } + + /** + * Returns {@code true} if the title is currently shown. + * + * @param title the title to check + * @return whether the title in currently shown + */ + public static boolean containsTitle(Title title) { + return titles.contains(title); + } + + /** + * Adds a title to be shown + * + * @param title the title to be shown + * @return whether the title is already currently being shown + */ + public static boolean addTitle(Title title) { + if (titles.add(title)) { + title.resetPos(); + return true; + } + return false; + } + + /** + * Adds a title to be shown for a set number of ticks + * + * @param title the title to be shown + * @param ticks the number of ticks to show the title + * @return whether the title is already currently being shown + */ + public static boolean addTitle(Title title, int ticks) { + if (addTitle(title)) { + Scheduler.INSTANCE.schedule(() -> TitleContainer.removeTitle(title), ticks); + return true; + } + return false; + } + + /** + * Stops showing a title + * + * @param title the title to stop showing + */ + public static void removeTitle(Title title) { + titles.remove(title); + } + + private static void render(DrawContext context, float tickDelta) { + render(context, titles, SkyblockerConfigManager.get().general.titleContainer.x, SkyblockerConfigManager.get().general.titleContainer.y, tickDelta); + } + + protected static void render(DrawContext context, Set<Title> titles, int xPos, int yPos, float tickDelta) { + var client = MinecraftClient.getInstance(); + TextRenderer textRenderer = client.textRenderer; + + // Calculate Scale to use + float scale = 3F * (SkyblockerConfigManager.get().general.titleContainer.titleContainerScale / 100F); + + // Grab direction and alignment values + SkyblockerConfig.Direction direction = SkyblockerConfigManager.get().general.titleContainer.direction; + SkyblockerConfig.Alignment alignment = SkyblockerConfigManager.get().general.titleContainer.alignment; + // x/y refer to the starting position for the text + // y always starts at yPos + float x = 0; + float y = yPos; + + //Calculate the width of combined text + float width = 0; + for (Title title : titles) { + width += textRenderer.getWidth(title.getText()) * scale + 10; + } + + if (alignment == SkyblockerConfig.Alignment.MIDDLE) { + if (direction == SkyblockerConfig.Direction.HORIZONTAL) { + //If middle aligned horizontally, start the xPosition at half of the width to the left. + x = xPos - (width / 2); + } else { + //If middle aligned vertically, start at xPos, we will shift each text to the left later + x = xPos; + } + } + if (alignment == SkyblockerConfig.Alignment.LEFT || alignment == SkyblockerConfig.Alignment.RIGHT) { + //If left or right aligned, start at xPos, we will shift each text later + x = xPos; + } + + for (Title title : titles) { + + //Calculate which x the text should use + float xToUse; + if (direction == SkyblockerConfig.Direction.HORIZONTAL) { + xToUse = alignment == SkyblockerConfig.Alignment.RIGHT ? + x - (textRenderer.getWidth(title.getText()) * scale) : //if right aligned we need the text position to be aligned on the right side. + x; + } else { + xToUse = alignment == SkyblockerConfig.Alignment.MIDDLE ? + x - (textRenderer.getWidth(title.getText()) * scale) / 2 : //if middle aligned we need the text position to be aligned in the middle. + alignment == SkyblockerConfig.Alignment.RIGHT ? + x - (textRenderer.getWidth(title.getText()) * scale) : //if right aligned we need the text position to be aligned on the right side. + x; + } + + //Start displaying the title at the correct position, not at the default position + if (title.isDefaultPos()) { + title.x = xToUse; + title.y = y; + } + + //Lerp the texts x and y variables + title.x = MathHelper.lerp(tickDelta * 0.5F, title.x, xToUse); + title.y = MathHelper.lerp(tickDelta * 0.5F, title.y, y); |
