aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/features/items
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/features/items')
-rw-r--r--src/main/kotlin/features/items/BlockZapperOverlay.kt139
-rw-r--r--src/main/kotlin/features/items/BonemerangOverlay.kt94
-rw-r--r--src/main/kotlin/features/items/EtherwarpOverlay.kt234
-rw-r--r--src/main/kotlin/features/items/recipes/ArrowWidget.kt38
-rw-r--r--src/main/kotlin/features/items/recipes/ComponentWidget.kt27
-rw-r--r--src/main/kotlin/features/items/recipes/EntityWidget.kt27
-rw-r--r--src/main/kotlin/features/items/recipes/FireWidget.kt19
-rw-r--r--src/main/kotlin/features/items/recipes/ItemList.kt308
-rw-r--r--src/main/kotlin/features/items/recipes/ItemSlotWidget.kt141
-rw-r--r--src/main/kotlin/features/items/recipes/MoulConfigWidget.kt46
-rw-r--r--src/main/kotlin/features/items/recipes/RecipeRegistry.kt116
-rw-r--r--src/main/kotlin/features/items/recipes/RecipeScreen.kt129
-rw-r--r--src/main/kotlin/features/items/recipes/RecipeWidget.kt37
-rw-r--r--src/main/kotlin/features/items/recipes/RenderableRecipe.kt27
-rw-r--r--src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt77
-rw-r--r--src/main/kotlin/features/items/recipes/TooltipWidget.kt30
16 files changed, 1489 insertions, 0 deletions
diff --git a/src/main/kotlin/features/items/BlockZapperOverlay.kt b/src/main/kotlin/features/items/BlockZapperOverlay.kt
new file mode 100644
index 0000000..cc58f8a
--- /dev/null
+++ b/src/main/kotlin/features/items/BlockZapperOverlay.kt
@@ -0,0 +1,139 @@
+package moe.nea.firmament.features.items
+
+import io.github.notenoughupdates.moulconfig.ChromaColour
+import java.util.LinkedList
+import net.minecraft.world.level.block.Block
+import net.minecraft.world.level.block.state.BlockState
+import net.minecraft.world.level.block.Blocks
+import net.minecraft.world.phys.BlockHitResult
+import net.minecraft.world.phys.HitResult
+import net.minecraft.core.BlockPos
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+
+object BlockZapperOverlay {
+ val identifier: String
+ get() = "block-zapper-overlay"
+
+ @Config
+ 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")
+ }
+
+ 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.level ?: return
+ val heldItem = MC.stackInHand
+ if (heldItem.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return
+ val hitResult = MC.instance.hitResult ?: 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.above()).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.offset(offset)
+
+ if (zapperBlocks.contains(newPos)) continue
+
+ val state: BlockState? = world.getBlockState(newPos)
+ if (state != null && state.block === block) {
+ val above = newPos.above()
+ 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?.isShiftKeyDown ?: 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")
+ }
+}
diff --git a/src/main/kotlin/features/items/BonemerangOverlay.kt b/src/main/kotlin/features/items/BonemerangOverlay.kt
new file mode 100644
index 0000000..3f16922
--- /dev/null
+++ b/src/main/kotlin/features/items/BonemerangOverlay.kt
@@ -0,0 +1,94 @@
+package moe.nea.firmament.features.items
+
+import me.shedaniel.math.Color
+import org.joml.Vector2i
+import net.minecraft.world.entity.LivingEntity
+import net.minecraft.world.entity.decoration.ArmorStand
+import net.minecraft.world.entity.player.Player
+import net.minecraft.ChatFormatting
+import net.minecraft.world.phys.AABB
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.EntityRenderTintEvent
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.render.TintedOverlayTexture
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+import moe.nea.firmament.util.tr
+
+object BonemerangOverlay {
+ val identifier: String
+ get() = "bonemerang-overlay"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.ITEMS) {
+ var bonemerangOverlay by toggle("bonemerang-overlay") { false }
+ val bonemerangOverlayHud by position("bonemerang-overlay-hud", 80, 10) { Vector2i() }
+ var highlightHitEntities by toggle("highlight-hit-entities") { false }
+ }
+
+ fun getEntities(): MutableSet<LivingEntity> {
+ val entities = mutableSetOf<LivingEntity>()
+ val camera = MC.camera as? Player ?: return entities
+ val player = MC.player ?: return entities
+ val world = player.level ?: return entities
+
+ val cameraPos = camera.eyePosition
+ val rayDirection = camera.lookAngle.normalize()
+ val endPos = cameraPos.add(rayDirection.scale(15.0))
+ val foundEntities = world.getEntities(camera, AABB(cameraPos, endPos).inflate(1.0))
+
+ for (entity in foundEntities) {
+ if (entity !is LivingEntity || entity is ArmorStand || entity.isInvisible) continue
+ val hitResult = entity.boundingBox.inflate(0.35).clip(cameraPos, endPos).orElse(null)
+ if (hitResult != null) entities.add(entity)
+ }
+
+ return entities
+ }
+
+
+ val throwableWeapons = listOf(
+ SkyBlockItems.BONE_BOOMERANG, SkyBlockItems.STARRED_BONE_BOOMERANG,
+ SkyBlockItems.TRIBAL_SPEAR,
+ )
+
+
+ @Subscribe
+ fun onEntityRender(event: EntityRenderTintEvent) {
+ if (!TConfig.highlightHitEntities) return
+ if (MC.stackInHand.skyBlockId !in throwableWeapons) return
+
+ val entities = getEntities()
+ if (entities.isEmpty()) return
+ if (event.entity !in entities) return
+
+ val tintOverlay by lazy {
+ TintedOverlayTexture().setColor(Color.ofOpaque(ChatFormatting.BLUE.color!!))
+ }
+
+ event.renderState.overlayTexture_firmament = tintOverlay
+ }
+
+
+ @Subscribe
+ fun onRenderHud(it: HudRenderEvent) {
+ if (!TConfig.bonemerangOverlay) return
+ if (MC.stackInHand.skyBlockId !in throwableWeapons) return
+
+ val entities = getEntities()
+
+ it.context.pose().pushMatrix()
+ TConfig.bonemerangOverlayHud.applyTransformations(it.context.pose())
+ it.context.drawString(
+ MC.font, String.format(
+ tr(
+ "firmament.bonemerang-overlay.bonemerang-overlay.display", "Bonemerang Targets: %s"
+ ).string, entities.size
+ ), 0, 0, -1, true
+ )
+ it.context.pose().popMatrix()
+ }
+}
diff --git a/src/main/kotlin/features/items/EtherwarpOverlay.kt b/src/main/kotlin/features/items/EtherwarpOverlay.kt
new file mode 100644
index 0000000..a59fcbd
--- /dev/null
+++ b/src/main/kotlin/features/items/EtherwarpOverlay.kt
@@ -0,0 +1,234 @@
+package moe.nea.firmament.features.items
+
+import io.github.notenoughupdates.moulconfig.ChromaColour
+import net.minecraft.world.level.block.Blocks
+import net.minecraft.core.Holder
+import net.minecraft.tags.BlockTags
+import net.minecraft.tags.TagKey
+import net.minecraft.network.chat.Component
+import net.minecraft.world.phys.BlockHitResult
+import net.minecraft.world.phys.HitResult
+import net.minecraft.core.BlockPos
+import net.minecraft.world.phys.Vec3
+import net.minecraft.world.phys.shapes.Shapes
+import net.minecraft.world.level.BlockGetter
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.extraAttributes
+import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+import moe.nea.firmament.util.tr
+
+object EtherwarpOverlay {
+ val identifier: String
+ get() = "etherwarp-overlay"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.ITEMS) {
+ var etherwarpOverlay by toggle("etherwarp-overlay") { false }
+ var onlyShowWhileSneaking by toggle("only-show-while-sneaking") { true }
+ var cube by toggle("cube") { true }
+ val cubeColour by colour("cube-colour") { ChromaColour.fromStaticRGB(172, 0, 255, 60) }
+ val failureCubeColour by colour("cube-colour-fail") { ChromaColour.fromStaticRGB(255, 0, 172, 60) }
+ val tooCloseCubeColour by colour("cube-colour-tooclose") { ChromaColour.fromStaticRGB(0, 255, 0, 60) }
+ val tooFarCubeColour by colour("cube-colour-toofar") { ChromaColour.fromStaticRGB(255, 255, 0, 60) }
+ var wireframe by toggle("wireframe") { false }
+ var failureText by toggle("failure-text") { false }
+ }
+
+ enum class EtherwarpResult(val label: Component?, val color: () -> ChromaColour) {
+ SUCCESS(null, TConfig::cubeColour),
+ INTERACTION_BLOCKED(
+ tr("firmament.etherwarp.fail.tooclosetointeractable", "Too close to interactable"),
+ TConfig::tooCloseCubeColour
+ ),
+ TOO_DISTANT(tr("firmament.etherwarp.fail.toofar", "Too far away"), TConfig::tooFarCubeColour),
+ OCCUPIED(tr("firmament.etherwarp.fail.occupied", "Occupied"), TConfig::failureCubeColour),
+ }
+
+ val interactionBlocked = Checker(
+ setOf(
+ Blocks.HOPPER,
+ Blocks.CHEST,
+ Blocks.ENDER_CHEST,
+ Blocks.FURNACE,
+ Blocks.CRAFTING_TABLE,
+ Blocks.CAULDRON,
+ Blocks.WATER_CAULDRON,
+ Blocks.ENCHANTING_TABLE,
+ Blocks.DISPENSER,
+ Blocks.DROPPER,
+ Blocks.BREWING_STAND,
+ Blocks.TRAPPED_CHEST,
+ Blocks.LEVER,
+ ),
+ setOf(
+ BlockTags.DOORS,
+ BlockTags.TRAPDOORS,
+ BlockTags.ANVIL,
+ BlockTags.FENCE_GATES,
+ )
+ )
+
+ data class Checker<T>(
+ val direct: Set<T>,
+ val byTag: Set<TagKey<T>>,
+ ) {
+ fun matches(entry: Holder<T>): Boolean {
+ return entry.value() in direct || checkTags(entry, byTag)
+ }
+ }
+
+ val etherwarpHallpasses = Checker(
+ setOf(
+ Blocks.CREEPER_HEAD,
+ Blocks.CREEPER_WALL_HEAD,
+ Blocks.DRAGON_HEAD,
+ Blocks.DRAGON_WALL_HEAD,
+ Blocks.SKELETON_SKULL,
+ Blocks.SKELETON_WALL_SKULL,
+ Blocks.WITHER_SKELETON_SKULL,
+ Blocks.WITHER_SKELETON_WALL_SKULL,
+ Blocks.PIGLIN_HEAD,
+ Blocks.PIGLIN_WALL_HEAD,
+ Blocks.ZOMBIE_HEAD,
+ Blocks.ZOMBIE_WALL_HEAD,
+ Blocks.PLAYER_HEAD,
+ Blocks.PLAYER_WALL_HEAD,
+ Blocks.REPEATER,
+ Blocks.COMPARATOR,
+ Blocks.BIG_DRIPLEAF_STEM,
+ Blocks.MOSS_CARPET,
+ Blocks.PALE_MOSS_CARPET,
+ Blocks.COCOA,
+ Blocks.LADDER,
+ Blocks.SEA_PICKLE,
+ ),
+ setOf(
+ BlockTags.FLOWER_POTS,
+ BlockTags.WOOL_CARPETS,
+ ),
+ )
+ val etherwarpConsidersFat = Checker(
+ setOf(), setOf(
+ // Wall signs have a hitbox
+ BlockTags.ALL_SIGNS, BlockTags.ALL_HANGING_SIGNS,
+ BlockTags.BANNERS,
+ )
+ )
+
+
+ fun <T> checkTags(holder: Holder<out T>, set: Set<TagKey<out T>>) =
+ holder.tags()
+ .anyMatch(set::contains)
+
+
+ fun isEtherwarpTransparent(world: BlockGetter, blockPos: BlockPos): Boolean {
+ val blockState = world.getBlockState(blockPos)
+ val block = blockState.block
+ if (etherwarpConsidersFat.matches(blockState.blockHolder))
+ return false
+ if (block.defaultBlockState().getCollisionShape(world, blockPos).isEmpty)
+ return true
+ if (etherwarpHallpasses.matches(blockState.blockHolder))
+ return true
+ return false
+ }
+
+ sealed interface EtherwarpBlockHit {
+ data class BlockHit(val blockPos: BlockPos, val accuratePos: Vec3?) : EtherwarpBlockHit
+ data object Miss : EtherwarpBlockHit
+ }
+
+ fun raycastWithEtherwarpTransparency(world: BlockGetter, start: Vec3, end: Vec3): EtherwarpBlockHit {
+ return BlockGetter.traverseBlocks<EtherwarpBlockHit, Unit>(
+ start, end, Unit,
+ { _, blockPos ->
+ if (isEtherwarpTransparent(world, blockPos)) {
+ return@traverseBlocks null
+ }
+// val defaultedState = world.getBlockState(blockPos).block.defaultState
+// val hitShape = defaultedState.getCollisionShape(
+// world,
+// blockPos,
+// ShapeContext.absent()
+// )
+// if (world.raycastBlock(start, end, blockPos, hitShape, defaultedState) == null) {
+// return@raycast null
+// }
+ val partialResult = world.clipWithInteractionOverride(start, end, blockPos, Shapes.block(), world.getBlockState(blockPos).block.defaultBlockState())
+ return@traverseBlocks EtherwarpBlockHit.BlockHit(blockPos, partialResult?.location)
+ },
+ { EtherwarpBlockHit.Miss })
+ }
+
+ enum class EtherwarpItemKind {
+ MERGED,
+ RAW
+ }
+
+ @Subscribe
+ fun renderEtherwarpOverlay(event: WorldRenderLastEvent) {
+ if (!TConfig.etherwarpOverlay) return
+ val player = MC.player ?: return
+ if (TConfig.onlyShowWhileSneaking && !player.isShiftKeyDown) return
+ val world = player.level
+ val heldItem = MC.stackInHand
+ val etherwarpTyp = run {
+ if (heldItem.extraAttributes.contains("ethermerge"))
+ EtherwarpItemKind.MERGED
+ else if (heldItem.skyBlockId == SkyBlockItems.ETHERWARP_CONDUIT)
+ EtherwarpItemKind.RAW
+ else
+ return
+ }
+ val playerEyeHeight = // Sneaking: 1.27 (1.21) 1.54 (1.8.9) / Upright: 1.62 (1.8.9,1.21)
+ if (player.isShiftKeyDown || etherwarpTyp == EtherwarpItemKind.MERGED)
+ (if (SBData.skyblockLocation?.isModernServer ?: false) 1.27 else 1.54)
+ else 1.62
+ val playerEyePos = player.position.add(0.0, playerEyeHeight, 0.0)
+ val start = playerEyePos
+ val end = player.getViewVector(0F).scale(160.0).add(playerEyePos)
+ val hitResult = raycastWithEtherwarpTransparency(
+ world,
+ start,
+ end,
+ )
+ if (hitResult !is EtherwarpBlockHit.BlockHit) return
+ val blockPos = hitResult.blockPos
+ val success = run {
+ if (!isEtherwarpTransparent(world, blockPos.above()))
+ EtherwarpResult.OCCUPIED
+ else if (!isEtherwarpTransparent(world, blockPos.above(2)))
+ EtherwarpResult.OCCUPIED
+ else if (playerEyePos.distanceToSqr(hitResult.accuratePos ?: blockPos.center) > 61 * 61)
+ EtherwarpResult.TOO_DISTANT
+ else if ((MC.instance.hitResult as? BlockHitResult)
+ ?.takeIf { it.type == HitResult.Type.BLOCK }
+ ?.let { interactionBlocked.matches(world.getBlockState(it.blockPos).blockHolder) }
+ ?: false
+ )
+ EtherwarpResult.INTERACTION_BLOCKED
+ else
+ EtherwarpResult.SUCCESS
+ }
+ RenderInWorldContext.renderInWorld(event) {
+ if (TConfig.cube)
+ block(
+ blockPos,
+ success.color().getEffectiveColourRGB()
+ )
+ if (TConfig.wireframe) wireframeCube(blockPos, 10f)
+ if (TConfig.failureText && success.label != null) {
+ withFacingThePlayer(blockPos.center) {
+ text(success.label)
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/ArrowWidget.kt b/src/main/kotlin/features/items/recipes/ArrowWidget.kt
new file mode 100644
index 0000000..db0cf60
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/ArrowWidget.kt
@@ -0,0 +1,38 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.renderer.RenderPipelines
+import net.minecraft.resources.ResourceLocation
+
+class ArrowWidget(override var position: Point) : RecipeWidget() {
+ override val size: Dimension
+ get() = Dimension(14, 14)
+
+ companion object {
+ val arrowSprite = ResourceLocation.withDefaultNamespace("container/furnace/lit_progress")
+ }
+
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ guiGraphics.blitSprite(
+ RenderPipelines.GUI_TEXTURED,
+ arrowSprite,
+ 14,
+ 14,
+ 0,
+ 0,
+ position.x,
+ position.y,
+ 14,
+ 14
+ )
+ }
+
+}
diff --git a/src/main/kotlin/features/items/recipes/ComponentWidget.kt b/src/main/kotlin/features/items/recipes/ComponentWidget.kt
new file mode 100644
index 0000000..08a2aa2
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/ComponentWidget.kt
@@ -0,0 +1,27 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.repo.recipes.RecipeLayouter
+import moe.nea.firmament.util.MC
+
+class ComponentWidget(override var position: Point, var text: Component) : RecipeWidget(), RecipeLayouter.Updater<Component> {
+ override fun update(newValue: Component) {
+ this.text = newValue
+ }
+
+ override val size: Dimension
+ get() = Dimension(MC.font.width(text), MC.font.lineHeight)
+
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ guiGraphics.drawString(MC.font, text, position.x, position.y, -1)
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/EntityWidget.kt b/src/main/kotlin/features/items/recipes/EntityWidget.kt
new file mode 100644
index 0000000..4a087e5
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/EntityWidget.kt
@@ -0,0 +1,27 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.world.entity.LivingEntity
+import moe.nea.firmament.gui.entity.EntityRenderer
+
+class EntityWidget(
+ override var position: Point,
+ override val size: Dimension,
+ val entity: LivingEntity
+) : RecipeWidget() {
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ EntityRenderer.renderEntity(
+ entity, guiGraphics,
+ rect.x, rect.y,
+ rect.width.toDouble(), rect.height.toDouble(),
+ mouseX.toDouble(), mouseY.toDouble()
+ )
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/FireWidget.kt b/src/main/kotlin/features/items/recipes/FireWidget.kt
new file mode 100644
index 0000000..565152b
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/FireWidget.kt
@@ -0,0 +1,19 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import net.minecraft.client.gui.GuiGraphics
+
+class FireWidget(override var position: Point, val animationTicks: Int) : RecipeWidget() {
+ override val size: Dimension
+ get() = Dimension(10, 10)
+
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ TODO("Not yet implemented")
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/ItemList.kt b/src/main/kotlin/features/items/recipes/ItemList.kt
new file mode 100644
index 0000000..340e1c3
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/ItemList.kt
@@ -0,0 +1,308 @@
+package moe.nea.firmament.features.items.recipes
+
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import io.github.notenoughupdates.moulconfig.observer.Property
+import java.util.Optional
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.components.Renderable
+import net.minecraft.client.gui.components.events.GuiEventListener
+import net.minecraft.client.gui.navigation.ScreenAxis
+import net.minecraft.client.gui.navigation.ScreenRectangle
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.client.input.MouseButtonEvent
+import net.minecraft.client.input.MouseButtonInfo
+import net.minecraft.network.chat.Component
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.api.v1.FirmamentAPI
+import moe.nea.firmament.events.HandledScreenClickEvent
+import moe.nea.firmament.events.HandledScreenForegroundEvent
+import moe.nea.firmament.events.ReloadRegistrationEvent
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.accessors.castAccessor
+import moe.nea.firmament.util.render.drawLine
+import moe.nea.firmament.util.skyblockId
+
+object ItemList {
+ // TODO: add a global toggle for this and RecipeRegistry
+
+ fun collectExclusions(screen: Screen): Set<ScreenRectangle> {
+ val exclusions = mutableSetOf<ScreenRectangle>()
+ if (screen is AbstractContainerScreen<*>) {
+ val screenHandler = screen.castAccessor()
+ exclusions.add(
+ ScreenRectangle(
+ screenHandler.x_Firmament,
+ screenHandler.y_Firmament,
+ screenHandler.backgroundWidth_Firmament,
+ screenHandler.backgroundHeight_Firmament
+ )
+ )
+ }
+ FirmamentAPI.getInstance().extensions
+ .forEach { extension ->
+ for (rectangle in extension.getExclusionZones(screen)) {
+ if (exclusions.any { it.encompasses(rectangle) })
+ continue
+ exclusions.add(rectangle)
+ }
+ }
+
+ return exclusions
+ }
+
+ var reachableItems = listOf<SBItemStack>()
+ var pageOffset = 0
+ fun recalculateVisibleItems() {
+ reachableItems = RepoManager.neuRepo.items
+ .items.values.map { SBItemStack(it.skyblockId) }
+ }
+
+ @Subscribe
+ fun onReload(event: ReloadRegistrationEvent) {
+ event.repo.registerReloadListener { recalculateVisibleItems() }
+ }
+
+ fun coordinates(outer: ScreenRectangle, exclusions: Collection<ScreenRectangle>): Sequence<ScreenRectangle> {
+ val entryWidth = 18
+ val columns = outer.width / entryWidth
+ val rows = outer.height / entryWidth
+ val lowX = outer.right() - columns * entryWidth
+ val lowY = outer.top()
+ return generateSequence(0) { it + 1 }
+ .map {
+ val xIndex = it % columns
+ val yIndex = it / columns
+ ScreenRectangle(
+ lowX + xIndex * entryWidth, lowY + yIndex * entryWidth,
+ entryWidth, entryWidth
+ )
+ }
+ .take(rows * columns)
+ .filter { candidate -> exclusions.none { it.intersects(candidate) } }
+ }
+
+ var lastRenderPositions: List<Pair<ScreenRectangle, SBItemStack>> = listOf()
+ var lastHoveredItemStack: Pair<ScreenRectangle, SBItemStack>? = null
+
+ abstract class ItemListElement(
+ ) : Renderable, GuiEventListener {
+ abstract val rectangle: Rectangle
+ override fun setFocused(focused: Boolean) {
+ }
+
+ override fun isFocused(): Boolean {
+ return false
+ }
+
+ override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
+ return rectangle.contains(mouseX, mouseY)
+ }
+ }
+
+ interface HasLabel {
+ fun component(): Component
+ }
+
+
+ class PopupSettingsElement<T : HasLabel>(
+ x: Int,
+ y: Int,
+ width: Int,
+ val selected: GetSetter<T>,
+ val options: List<T>,
+ ) : ItemListElement() {
+ override val rectangle: Rectangle = Rectangle(x, y, width, 4 + (MC.font.lineHeight + 2) * options.size)
+ fun bb(i: Int) =
+ Rectangle(
+ rectangle.minX, rectangle.minY + (2 + MC.font.lineHeight) * i + 2,
+ rectangle.width, MC.font.lineHeight
+ )
+
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ guiGraphics.fill(rectangle.minX, rectangle.minY, rectangle.maxX, rectangle.maxY, 0xFF000000.toInt())
+ guiGraphics.submitOutline(rectangle.x, rectangle.y, rectangle.width, rectangle.height, -1)
+ val sel = selected.get()
+ for ((index, element) in options.withIndex()) {
+ val b = bb(index)
+ val tw = MC.font.width(element.component())
+ guiGraphics.drawString(
+ MC.font, element.component(), b.centerX - tw / 2,
+ b.y + 1,
+ if (element == sel) 0xFFA0B000.toInt() else -1
+ )
+ if (b.contains(mouseX, mouseY))
+ guiGraphics.hLine(b.centerX - tw / 2, b.centerX + tw / 2 - 1, b.maxY + 1, -1)
+ }
+ }
+
+ override fun mouseClicked(event: MouseButtonEvent, isDoubleClick: Boolean): Boolean {
+ popupElement = null
+ for ((index, element) in options.withIndex()) {
+ val b = bb(index)
+ if (b.contains(event.x, event.y)) {
+ selected.set(element)
+ break
+ }
+ }
+ return true
+ }
+ }
+
+ class SettingElement<T : HasLabel>(
+ x: Int,
+ y: Int,
+ val selected: GetSetter<T>,
+ val options: List<T>
+ ) : ItemListElement() {
+ val height = MC.font.lineHeight + 4
+ val width = options.maxOf { MC.font.width(it.component()) } + 4
+ override val rectangle: Rectangle = Rectangle(x, y, width, height)
+
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ guiGraphics.drawCenteredString(MC.font, selected.get().component(), rectangle.centerX, rectangle.y + 2, -1)
+ if (isMouseOver(mouseX.toDouble(), mouseY.toDouble())) {
+ guiGraphics.hLine(rectangle.minX, rectangle.maxX - 1, rectangle.maxY - 2, -1)
+ }
+ }
+
+ override fun mouseClicked(
+ event: MouseButtonEvent,
+ isDoubleClick: Boolean
+ ): Boolean {
+ popupElement = PopupSettingsElement(
+ rectangle.x,
+ rectangle.y - options.size * (MC.font.lineHeight + 2) - 2,
+ width,
+ selected,
+ options
+ )
+ return true
+ }
+ }
+
+ var popupElement: ItemListElement? = null
+
+
+ fun findStackUnder(mouseX: Int, mouseY: Int): Pair<ScreenRectangle, SBItemStack>? {
+ val lhis = lastHoveredItemStack
+ if (lhis != null && lhis.first.containsPoint(mouseX, mouseY))
+ return lhis
+ return lastRenderPositions.firstOrNull { it.first.containsPoint(mouseX, mouseY) }
+ }
+
+ val isItemListEnabled get() = false
+
+ @Subscribe
+ fun onClick(event: HandledScreenClickEvent) {
+ if(!isItemListEnabled)return
+ val pe = popupElement
+ val me = MouseButtonEvent(
+ event.mouseX, event.mouseY,
+ MouseButtonInfo(event.button, 0) // TODO: missing modifiers
+ )
+ if (pe != null) {
+ event.cancel()
+ if (!pe.isMouseOver(event.mouseX, event.mouseY)) {
+ popupElement = null
+ return
+ }
+ pe.mouseClicked(
+ me,
+ false
+ )
+ return
+ }
+ listElements.forEach {
+ if (it.isMouseOver(event.mouseX, event.mouseY))
+ it.mouseClicked(me, false)
+ }
+ }
+
+ var listElements = listOf<ItemListElement>()
+
+ @Subscribe
+ fun onRender(event: HandledScreenForegroundEvent) {
+ if(!isItemListEnabled) return
+ lastHoveredItemStack = null
+ lastRenderPositions = listOf()
+ val exclusions = collectExclusions(event.screen)
+ val potentiallyVisible = reachableItems.subList(pageOffset, reachableItems.size)
+ val screenWidth = event.screen.width
+ val rightThird = ScreenRectangle(
+ screenWidth - screenWidth / 3, 0,
+ screenWidth / 3, event.screen.height - MC.font.lineHeight - 4
+ )
+ val coords = coordinates(rightThird, exclusions)
+
+ lastRenderPositions = coords.zip(potentiallyVisible.asSequence()).toList()
+ val isPopupHovered = popupElement?.isMouseOver(event.mouseX.toDouble(),event.mouseY.toDouble())
+ ?: false
+ lastRenderPositions.forEach { (pos, stack) ->
+ val realStack = stack.asLazyImmutableItemStack()
+ val toRender = realStack ?: ItemStack(Items.PAINTING)
+ event.context.renderItem(toRender, pos.left() + 1, pos.top() + 1)
+ if (!isPopupHovered && pos.containsPoint(event.mouseX, event.mouseY)) {
+ lastHoveredItemStack = pos to stack
+ event.context.setTooltipForNextFrame(
+ MC.font,
+ if (realStack != null)
+ ItemSlotWidget.getTooltip(realStack)
+ else
+ stack.estimateLore(),
+ Optional.empty(),
+ event.mouseX, event.mouseY
+ )
+ }
+ }
+ event.context.fill(
+ rightThird.left(),
+ rightThird.bottom(),
+ rightThird.right(),
+ event.screen.height,
+ 0xFF000000.toInt()
+ )
+ val le = mutableListOf<ItemListElement>()
+ le.add(
+ SettingElement(
+ 0,
+ rightThird.bottom(),
+ sortOrder,
+ SortOrder.entries
+ )
+ )
+ val bottomWidth = le.sumOf { it.rectangle.width + 2 } - 2
+ var startX = rightThird.getCenterInAxis(ScreenAxis.HORIZONTAL) - bottomWidth / 2
+ le.forEach {
+ it.rectangle.translate(startX, 0)
+ startX += it.rectangle.width + 2
+ }
+ le.forEach { it.render(event.context, event.mouseX, event.mouseY, event.delta) }
+ listElements = le
+ popupElement?.render(event.context, event.mouseX, event.mouseY, event.delta)
+ }
+
+ enum class SortOrder(val component: Component) : HasLabel {
+ NAME(Component.literal("Name")),
+ RARITY(Component.literal("Rarity"));
+
+ override fun component(): Component = component
+ }
+
+ val sortOrder = Property.of(SortOrder.NAME)
+}
diff --git a/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt b/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt
new file mode 100644
index 0000000..b659643
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt
@@ -0,0 +1,141 @@
+package moe.nea.firmament.features.items.recipes
+
+import java.util.Optional
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback
+import net.minecraft.client.gui.GuiGraphics