feat: block zapper overlay (#208)

Co-authored-by: Linnea Gräf <nea@nea.moe>
This commit is contained in:
Jacob
2025-08-12 07:27:04 +08:00
committed by GitHub
parent 92fb76bb18
commit 98a0800996
5 changed files with 235 additions and 35 deletions

View File

@@ -0,0 +1,146 @@
package moe.nea.firmament.features.items
import io.github.notenoughupdates.moulconfig.ChromaColour
import java.util.LinkedList
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.hit.HitResult
import net.minecraft.util.math.BlockPos
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ClientStartedEvent
import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.render.RenderInWorldContext
import moe.nea.firmament.util.skyBlockId
import moe.nea.firmament.util.skyblock.SkyBlockItems
object BlockZapperOverlay : FirmamentFeature {
override val identifier: String
get() = "block-zapper-overlay"
object TConfig : ManagedConfig(identifier, Category.ITEMS) {
var blockZapperOverlay by toggle("block-zapper-overlay") { false }
val color by colour("color") { ChromaColour.fromStaticRGB(160, 0, 0, 60) }
var undoKey by keyBindingWithDefaultUnbound("undo-key")
}
@Subscribe
fun onInit(event: ClientStartedEvent) {
}
override val config: ManagedConfig
get() = TConfig
val bannedZapper: List<Block> = listOf<Block>(
Blocks.WHEAT,
Blocks.CARROTS,
Blocks.POTATOES,
Blocks.PUMPKIN,
Blocks.PUMPKIN_STEM,
Blocks.MELON,
Blocks.MELON_STEM,
Blocks.CACTUS,
Blocks.SUGAR_CANE,
Blocks.NETHER_WART,
Blocks.TALL_GRASS,
Blocks.SUNFLOWER,
Blocks.FARMLAND,
Blocks.BREWING_STAND,
Blocks.SNOW,
Blocks.RED_MUSHROOM,
Blocks.BROWN_MUSHROOM,
)
private val zapperOffsets: List<BlockPos> = listOf(
BlockPos(0, 0, -1),
BlockPos(0, 0, 1),
BlockPos(-1, 0, 0),
BlockPos(1, 0, 0),
BlockPos(0, 1, 0),
BlockPos(0, -1, 0)
)
// Skidded from NEU
// Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CustomItemEffects.java#L1281-L1355 (Modified)
@Subscribe
fun renderBlockZapperOverlay(event: WorldRenderLastEvent) {
if (!TConfig.blockZapperOverlay) return
val player = MC.player ?: return
val world = player.world ?: return
val heldItem = MC.stackInHand
if (heldItem.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return
val hitResult = MC.instance.crosshairTarget ?: return
val zapperBlocks: HashSet<BlockPos> = HashSet()
val returnablePositions = LinkedList<BlockPos>()
if (hitResult is BlockHitResult && hitResult.type == HitResult.Type.BLOCK) {
var pos: BlockPos = hitResult.blockPos
val firstBlockState: BlockState = world.getBlockState(pos)
val block = firstBlockState.block
val initialAboveBlock = world.getBlockState(pos.up()).block
if (!bannedZapper.contains(initialAboveBlock) && !bannedZapper.contains(block)) {
var i = 0
while (i < 164) {
zapperBlocks.add(pos)
returnablePositions.remove(pos)
val availableNeighbors: MutableList<BlockPos> = ArrayList()
for (offset in zapperOffsets) {
val newPos = pos.add(offset)
if (zapperBlocks.contains(newPos)) continue
val state: BlockState? = world.getBlockState(newPos)
if (state != null && state.block === block) {
val above = newPos.up()
val aboveBlock = world.getBlockState(above).block
if (!bannedZapper.contains(aboveBlock)) {
availableNeighbors.add(newPos)
}
}
}
if (availableNeighbors.size >= 2) {
returnablePositions.add(pos)
pos = availableNeighbors[0]
} else if (availableNeighbors.size == 1) {
pos = availableNeighbors[0]
} else if (returnablePositions.isEmpty()) {
break
} else {
i--
pos = returnablePositions.last()
}
i++
}
}
RenderInWorldContext.renderInWorld(event) {
if (MC.player?.isSneaking ?: false) {
zapperBlocks.forEach {
block(it, TConfig.color.getEffectiveColourRGB())
}
} else {
sharedVoxelSurface(zapperBlocks, TConfig.color.getEffectiveColourRGB())
}
}
}
}
@Subscribe
fun onWorldKeyboard(it: WorldKeyboardEvent) {
if (!TConfig.undoKey.isBound) return
if (!it.matches(TConfig.undoKey)) return
if (MC.stackInHand.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return
MC.sendCommand("undozap")
}
}

View File

@@ -88,6 +88,7 @@ object CustomRenderLayers {
val COLORED_QUADS = RenderLayer.of(
"firmament_quads",
RenderLayer.DEFAULT_BUFFER_SIZE,
false, true,
CustomRenderPipelines.COLORED_OMNIPRESENT_QUADS,
RenderLayer.MultiPhaseParameters.builder()
.lightmap(RenderPhase.DISABLE_LIGHTMAP)

View File

@@ -11,7 +11,6 @@ import net.minecraft.client.render.RenderLayer
import net.minecraft.client.render.RenderTickCounter
import net.minecraft.client.render.VertexConsumer
import net.minecraft.client.render.VertexConsumerProvider
import net.minecraft.client.render.VertexFormats
import net.minecraft.client.texture.Sprite
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
@@ -20,7 +19,6 @@ import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3d
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.util.FirmFormatters
import moe.nea.firmament.util.IntUtil.toRGBA
import moe.nea.firmament.util.MC
@RenderContextDSL
@@ -49,6 +47,38 @@ class RenderInWorldContext private constructor(
matrixStack.pop()
}
fun sharedVoxelSurface(blocks: Set<BlockPos>, color: Int) {
val m = BlockPos.Mutable()
val l = vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS)
blocks.forEach {
matrixStack.push()
matrixStack.translate(it.x.toFloat(), it.y.toFloat(), it.z.toFloat())
val p = matrixStack.peek().positionMatrix
m.set(it)
if (m.setX(it.x + 1) !in blocks) {
buildFaceXP(p, l, color)
}
if (m.setX(it.x - 1) !in blocks) {
buildFaceXN(p, l, color)
}
m.set(it)
if (m.setY(it.y + 1) !in blocks) {
buildFaceYP(p, l, color)
}
if (m.setY(it.y - 1) !in blocks) {
buildFaceYN(p, l, color)
}
m.set(it)
if (m.setZ(it.z + 1) !in blocks) {
buildFaceZP(p, l, color)
}
if (m.setZ(it.z - 1) !in blocks) {
buildFaceZN(p, l, color)
}
matrixStack.pop()
}
}
enum class VerticalAlign {
TOP, BOTTOM, CENTER;
@@ -205,41 +235,56 @@ class RenderInWorldContext private constructor(
}
}
private fun buildCube(matrix: Matrix4f, buf: VertexConsumer, colorInt: Int) {
val (r, g, b, a) = colorInt.toRGBA()
// Y-
buf.vertex(matrix, 0F, 0F, 0F).color(r, g, b, a)
buf.vertex(matrix, 0F, 0F, 1F).color(r, g, b, a)
buf.vertex(matrix, 1F, 0F, 1F).color(r, g, b, a)
buf.vertex(matrix, 1F, 0F, 0F).color(r, g, b, a)
// Y+
buf.vertex(matrix, 0F, 1F, 0F).color(r, g, b, a)
buf.vertex(matrix, 1F, 1F, 0F).color(r, g, b, a)
buf.vertex(matrix, 1F, 1F, 1F).color(r, g, b, a)
buf.vertex(matrix, 0F, 1F, 1F).color(r, g, b, a)
// X-
buf.vertex(matrix, 0F, 0F, 0F).color(r, g, b, a)
buf.vertex(matrix, 0F, 0F, 1F).color(r, g, b, a)
buf.vertex(matrix, 0F, 1F, 1F).color(r, g, b, a)
buf.vertex(matrix, 0F, 1F, 0F).color(r, g, b, a)
// X+
buf.vertex(matrix, 1F, 0F, 0F).color(r, g, b, a)
buf.vertex(matrix, 1F, 1F, 0F).color(r, g, b, a)
buf.vertex(matrix, 1F, 1F, 1F).color(r, g, b, a)
buf.vertex(matrix, 1F, 0F, 1F).color(r, g, b, a)
// Z-
buf.vertex(matrix, 0F, 0F, 0F).color(r, g, b, a)
buf.vertex(matrix, 1F, 0F, 0F).color(r, g, b, a)
buf.vertex(matrix, 1F, 1F, 0F).color(r, g, b, a)
buf.vertex(matrix, 0F, 1F, 0F).color(r, g, b, a)
// Z+
buf.vertex(matrix, 0F, 0F, 1F).color(r, g, b, a)
buf.vertex(matrix, 0F, 1F, 1F).color(r, g, b, a)
buf.vertex(matrix, 1F, 1F, 1F).color(r, g, b, a)
buf.vertex(matrix, 1F, 0F, 1F).color(r, g, b, a)
private fun buildFaceZP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
buf.vertex(matrix, 0F, 0F, 1F).color(rgba)
buf.vertex(matrix, 0F, 1F, 1F).color(rgba)
buf.vertex(matrix, 1F, 1F, 1F).color(rgba)
buf.vertex(matrix, 1F, 0F, 1F).color(rgba)
}
private fun buildFaceZN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
buf.vertex(matrix, 0F, 0F, 0F).color(rgba)
buf.vertex(matrix, 1F, 0F, 0F).color(rgba)
buf.vertex(matrix, 1F, 1F, 0F).color(rgba)
buf.vertex(matrix, 0F, 1F, 0F).color(rgba)
}
private fun buildFaceXP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
buf.vertex(matrix, 1F, 0F, 0F).color(rgba)
buf.vertex(matrix, 1F, 1F, 0F).color(rgba)
buf.vertex(matrix, 1F, 1F, 1F).color(rgba)
buf.vertex(matrix, 1F, 0F, 1F).color(rgba)
}
private fun buildFaceXN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
buf.vertex(matrix, 0F, 0F, 0F).color(rgba)
buf.vertex(matrix, 0F, 0F, 1F).color(rgba)
buf.vertex(matrix, 0F, 1F, 1F).color(rgba)
buf.vertex(matrix, 0F, 1F, 0F).color(rgba)
}
private fun buildFaceYN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
buf.vertex(matrix, 0F, 0F, 0F).color(rgba)
buf.vertex(matrix, 0F, 0F, 1F).color(rgba)
buf.vertex(matrix, 1F, 0F, 1F).color(rgba)
buf.vertex(matrix, 1F, 0F, 0F).color(rgba)
}
private fun buildFaceYP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
buf.vertex(matrix, 0F, 1F, 0F).color(rgba)
buf.vertex(matrix, 1F, 1F, 0F).color(rgba)
buf.vertex(matrix, 1F, 1F, 1F).color(rgba)
buf.vertex(matrix, 0F, 1F, 1F).color(rgba)
}
private fun buildCube(matrix4f: Matrix4f, buf: VertexConsumer, rgba: Int) {
buildFaceXP(matrix4f, buf, rgba)
buildFaceXN(matrix4f, buf, rgba)
buildFaceYP(matrix4f, buf, rgba)
buildFaceYN(matrix4f, buf, rgba)
buildFaceZP(matrix4f, buf, rgba)
buildFaceZN(matrix4f, buf, rgba)
}
fun renderInWorld(event: WorldRenderLastEvent, block: RenderInWorldContext. () -> Unit) {
// TODO: there should be *no more global state*. the only thing we should be doing is render layers. that includes settings like culling, blending, shader color, and depth testing

View File

@@ -19,5 +19,6 @@ object SkyBlockItems {
val BONE_BOOMERANG = SkyblockId("BONE_BOOMERANG")
val STARRED_BONE_BOOMERANG = SkyblockId("STARRED_BONE_BOOMERANG")
val TRIBAL_SPEAR = SkyblockId("TRIBAL_SPEAR")
val BLOCK_ZAPPER = SkyblockId("BLOCK_ZAPPER")
val HUNTING_TOOLKIT = SkyblockId("HUNTING_TOOLKIT")
}

View File

@@ -24,6 +24,13 @@
"firmament.config.auto-completions.warp-complete.description": "Auto complete warp destinations in chat. This may include warps you have not yet unlocked.",
"firmament.config.auto-completions.warp-is": "Redirect /warp is to /warp island",
"firmament.config.auto-completions.warp-is.description": "Redirects /warp is to /warp island, since hypixel does not recognize /warp is as a warp destination.",
"firmament.config.block-zapper-overlay": "Block Zapper Overlay",
"firmament.config.block-zapper-overlay.block-zapper-overlay": "Block Zapper Overlay",
"firmament.config.block-zapper-overlay.block-zapper-overlay.description": "Shows what blocks will be zapped",
"firmament.config.block-zapper-overlay.color": "Colour",
"firmament.config.block-zapper-overlay.color.description": "The color that the blocks will be highlighted in",
"firmament.config.block-zapper-overlay.undo-key": "Undo Keybind",
"firmament.config.block-zapper-overlay.undo-key.description": "Keybind to undo your zap",
"firmament.config.bonemerang-overlay": "Bonemerang Overlay",
"firmament.config.bonemerang-overlay.bonemerang-overlay": "Bonemerang Overlay",
"firmament.config.bonemerang-overlay.bonemerang-overlay-hud": "Bonemerang Overlay Hud",