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
+import net.minecraft.network.chat.Component
+import net.minecraft.world.item.Item
+import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.TooltipFlag
+import moe.nea.firmament.api.v1.FirmamentItemWidget
+import moe.nea.firmament.events.ItemTooltipEvent
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.repo.recipes.RecipeLayouter
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.FirmFormatters.shortFormat
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.darkGrey
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+
+class ItemSlotWidget(
+ point: Point,
+ var content: List<SBItemStack>,
+ val slotKind: RecipeLayouter.SlotKind
+) : RecipeWidget(),
+ RecipeLayouter.CyclingItemSlot,
+ FirmamentItemWidget {
+ override var position = point
+ override val size get() = Dimension(16, 16)
+ val itemRect get() = Rectangle(position, Dimension(16, 16))
+
+ val backgroundTopLeft
+ get() =
+ if (slotKind.isBig) Point(position.x - 4, position.y - 4)
+ else Point(position.x - 1, position.y - 1)
+ val backgroundSize =
+ if (slotKind.isBig) Dimension(16 + 8, 16 + 8)
+ else Dimension(18, 18)
+ override val rect: Rectangle
+ get() = Rectangle(backgroundTopLeft, backgroundSize)
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ val stack = current().asImmutableItemStack()
+ // TODO: draw slot background
+ if (stack.isEmpty) return
+ guiGraphics.renderItem(stack, position.x, position.y)
+ guiGraphics.renderItemDecorations(
+ MC.font, stack, position.x, position.y,
+ if (stack.count >= SHORT_NUM_CUTOFF) shortFormat(stack.count.toDouble())
+ else null
+ )
+ if (itemRect.contains(mouseX, mouseY)
+ && guiGraphics.containsPointInScissor(mouseX, mouseY)
+ ) guiGraphics.setTooltipForNextFrame(
+ MC.font, getTooltip(stack), Optional.empty(),
+ mouseX, mouseY
+ )
+ }
+
+ companion object {
+ val SHORT_NUM_CUTOFF = 1000
+ var canUseTooltipEvent = true
+
+ fun getTooltip(itemStack: ItemStack): List<Component> {
+ val lore = mutableListOf(itemStack.displayNameAccordingToNbt)
+ lore.addAll(itemStack.loreAccordingToNbt)
+ if (canUseTooltipEvent) {
+ try {
+ ItemTooltipCallback.EVENT.invoker().getTooltip(
+ itemStack, Item.TooltipContext.EMPTY,
+ TooltipFlag.NORMAL, lore
+ )
+ } catch (ex: Exception) {
+ canUseTooltipEvent = false
+ ErrorUtil.softError("Failed to use vanilla tooltips", ex)
+ }
+ } else {
+ ItemTooltipEvent.publish(
+ ItemTooltipEvent(
+ itemStack,
+ Item.TooltipContext.EMPTY,
+ TooltipFlag.NORMAL,
+ lore
+ )
+ )
+ }
+ if (itemStack.count >= SHORT_NUM_CUTOFF && lore.isNotEmpty())
+ lore.add(1, Component.literal("${itemStack.count}x").darkGrey())
+ return lore
+ }
+ }
+
+
+ override fun tick() {
+ if (SavedKeyBinding.isShiftDown()) return
+ if (content.size <= 1) return
+ if (MC.currentTick % 5 != 0) return
+ index = (index + 1) % content.size
+ }
+
+ var index = 0
+ var onUpdate: () -> Unit = {}
+
+ override fun onUpdate(action: () -> Unit) {
+ this.onUpdate = action
+ }
+
+ override fun current(): SBItemStack {
+ return content.getOrElse(index) { SBItemStack.EMPTY }
+ }
+
+ override fun update(newValue: SBItemStack) {
+ content = listOf(newValue)
+ // SAFE: content was just assigned to a non-empty list
+ index = index.coerceIn(content.indices)
+ }
+
+ override fun getPlacement(): FirmamentItemWidget.Placement {
+ return FirmamentItemWidget.Placement.RECIPE_SCREEN
+ }
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ override fun getItemStack(): ItemStack {
+ return current().asImmutableItemStack()
+ }
+
+ override fun getSkyBlockId(): String {
+ return current().skyblockId.neuItem
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt b/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt
new file mode 100644
index 0000000..aad3bda
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt
@@ -0,0 +1,46 @@
+package moe.nea.firmament.features.items.recipes
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import io.github.notenoughupdates.moulconfig.gui.MouseEvent
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.input.MouseButtonEvent
+import moe.nea.firmament.util.MoulConfigUtils.createAndTranslateFullContext
+
+class MoulConfigWidget(
+ val component: GuiComponent,
+ override var position: Point,
+ override val size: Dimension,
+) : RecipeWidget() {
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ createAndTranslateFullContext(
+ guiGraphics, mouseX, mouseY, rect,
+ component::render
+ )
+ }
+
+ override fun mouseClicked(event: MouseButtonEvent, isDoubleClick: Boolean): Boolean {
+ return createAndTranslateFullContext(null, event.x.toInt(), event.y.toInt(), rect) {
+ component.mouseEvent(MouseEvent.Click(event.button(), true), it)
+ }
+ }
+
+ override fun mouseMoved(mouseX: Double, mouseY: Double) {
+ createAndTranslateFullContext(null, mouseX, mouseY, rect) {
+ component.mouseEvent(MouseEvent.Move(0F, 0F), it)
+ }
+ }
+
+ override fun mouseReleased(event: MouseButtonEvent): Boolean {
+ return createAndTranslateFullContext(null, event.x, event.y, rect) {
+ component.mouseEvent(MouseEvent.Click(event.button(), false), it)
+ }
+ }
+
+}
diff --git a/src/main/kotlin/features/items/recipes/RecipeRegistry.kt b/src/main/kotlin/features/items/recipes/RecipeRegistry.kt
new file mode 100644
index 0000000..c2df46f
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/RecipeRegistry.kt
@@ -0,0 +1,116 @@
+package moe.nea.firmament.features.items.recipes
+
+import com.mojang.blaze3d.platform.InputConstants
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.events.ReloadRegistrationEvent
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.repo.recipes.GenericRecipeRenderer
+import moe.nea.firmament.repo.recipes.SBCraftingRecipeRenderer
+import moe.nea.firmament.repo.recipes.SBEssenceUpgradeRecipeRenderer
+import moe.nea.firmament.repo.recipes.SBForgeRecipeRenderer
+import moe.nea.firmament.repo.recipes.SBReforgeRecipeRenderer
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.focusedItemStack
+
+object RecipeRegistry {
+ val recipeTypes: List<GenericRecipeRenderer<*>> = listOf(
+ SBCraftingRecipeRenderer,
+ SBForgeRecipeRenderer,
+ SBReforgeRecipeRenderer,
+ SBEssenceUpgradeRecipeRenderer,
+ )
+
+
+ @Subscribe
+ fun showUsages(event: HandledScreenKeyPressedEvent) {
+ val provider =
+ if (event.matches(SavedKeyBinding.keyWithoutMods(InputConstants.KEY_R))) {
+ ::getRecipesFor
+ } else if (event.matches(SavedKeyBinding.keyWithoutMods(InputConstants.KEY_U))) {
+ ::getUsagesFor
+ } else {
+ return
+ }
+ val stack = event.screen.focusedItemStack ?: return
+ val recipes = provider(SBItemStack(stack))
+ if (recipes.isEmpty()) return
+ MC.screen = RecipeScreen(recipes.toList())
+ }
+
+
+ object RecipeIndexes : IReloadable {
+
+ private fun <T : Any> createIndexFor(
+ neuRepository: NEURepository,
+ recipeRenderer: GenericRecipeRenderer<T>,
+ outputs: Boolean,
+ ): List<Pair<SkyblockId, RenderableRecipe<T>>> {
+ val indexer: (T) -> Collection<SBItemStack> =
+ if (outputs) recipeRenderer::getOutputs
+ else recipeRenderer::getInputs
+ return recipeRenderer.findAllRecipes(neuRepository)
+ .flatMap {
+ val wrappedRecipe = RenderableRecipe(it, recipeRenderer, null)
+ indexer(it).map { it.skyblockId to wrappedRecipe }
+ }
+ }
+
+ fun createIndex(outputs: Boolean): MutableMap<SkyblockId, List<RenderableRecipe<*>>> {
+ val m: MutableMap<SkyblockId, List<RenderableRecipe<*>>> = mutableMapOf()
+ recipeTypes.forEach { renderer ->
+ createIndexFor(RepoManager.neuRepo, renderer, outputs)
+ .forEach { (stack, recipe) ->
+ m.merge(stack, listOf(recipe)) { a, b -> a + b }
+ }
+ }
+ return m
+ }
+
+ lateinit var recipesForIndex: Map<SkyblockId, List<RenderableRecipe<*>>>
+ lateinit var usagesForIndex: Map<SkyblockId, List<RenderableRecipe<*>>>
+ override fun reload(recipe: NEURepository) {
+ recipesForIndex = createIndex(true)
+ usagesForIndex = createIndex(false)
+ }
+ }
+
+ @Subscribe
+ fun onRepoBuild(event: ReloadRegistrationEvent) {
+ event.repo.registerReloadListener(RecipeIndexes)
+ }
+
+
+ fun getRecipesFor(itemStack: SBItemStack): Set<RenderableRecipe<*>> {
+ val recipes = LinkedHashSet<RenderableRecipe<*>>()
+ recipeTypes.forEach { injectRecipesFor(it, recipes, itemStack, true) }
+ recipes.addAll(RecipeIndexes.recipesForIndex[itemStack.skyblockId] ?: emptyList())
+ return recipes
+ }
+
+ fun getUsagesFor(itemStack: SBItemStack): Set<RenderableRecipe<*>> {
+ val recipes = LinkedHashSet<RenderableRecipe<*>>()
+ recipeTypes.forEach { injectRecipesFor(it, recipes, itemStack, false) }
+ recipes.addAll(RecipeIndexes.usagesForIndex[itemStack.skyblockId] ?: emptyList())
+ return recipes
+ }
+
+ private fun <T : Any> injectRecipesFor(
+ recipeRenderer: GenericRecipeRenderer<T>,
+ collector: MutableCollection<RenderableRecipe<*>>,
+ relevantItem: SBItemStack,
+ mustBeInOutputs: Boolean
+ ) {
+ collector.addAll(
+ recipeRenderer.discoverExtraRecipes(RepoManager.neuRepo, relevantItem, mustBeInOutputs)
+ .map { RenderableRecipe(it, recipeRenderer, relevantItem) }
+ )
+ }
+
+
+}
diff --git a/src/main/kotlin/features/items/recipes/RecipeScreen.kt b/src/main/kotlin/features/items/recipes/RecipeScreen.kt
new file mode 100644
index 0000000..9a746f3
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/RecipeScreen.kt
@@ -0,0 +1,129 @@
+package moe.nea.firmament.features.items.recipes
+
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.client.renderer.RenderPipelines
+import moe.nea.firmament.util.mc.CommonTextures
+import moe.nea.firmament.util.render.enableScissorWithTranslation
+import moe.nea.firmament.util.tr
+
+class RecipeScreen(
+ val recipes: List<RenderableRecipe<*>>,
+) : Screen(tr("firmament.recipe.screen", "SkyBlock Recipe")) {
+
+ data class PlacedRecipe(
+ val bounds: Rectangle,
+ val layoutedRecipe: StandaloneRecipeRenderer,
+ ) {
+ fun moveTo(position: Point) {
+ val Δx = position.x - bounds.x
+ val Δy = position.y - bounds.y
+ bounds.translate(Δx, Δy)
+ layoutedRecipe.widgets.forEach { widget ->
+ widget.position = widget.position.clone().also {
+ it.translate(Δx, Δy)
+ }
+ }
+ }
+ }
+
+ lateinit var placedRecipes: List<PlacedRecipe>
+ var scrollViewport: Int = 0
+ var scrollOffset: Int = 0
+ var scrollPortWidth: Int = 0
+ var heightEstimate: Int = 0
+ val gutter = 10
+ override fun init() {
+ super.init()
+ scrollViewport = minOf(height - 20, 250)
+ scrollPortWidth = 0
+ heightEstimate = 0
+ var offset = height / 2 - scrollViewport / 2
+ placedRecipes = recipes.map {
+ val effectiveWidth = minOf(it.renderer.displayWidth, width - 20)
+ val bounds = Rectangle(
+ width / 2 - effectiveWidth / 2,
+ offset,
+ effectiveWidth,
+ it.renderer.displayHeight
+ )
+ if (heightEstimate > 0)
+ heightEstimate += gutter
+ heightEstimate += bounds.height
+ scrollPortWidth = maxOf(effectiveWidth, scrollPortWidth)
+ offset += bounds.height + gutter
+ val layoutedRecipe = it.render(bounds)
+ layoutedRecipe.widgets.forEach(this::addRenderableWidget)
+ PlacedRecipe(bounds, layoutedRecipe)
+ }
+ }
+
+ fun scrollRect() =
+ Rectangle(
+ width / 2 - scrollPortWidth / 2, height / 2 - scrollViewport / 2,
+ scrollPortWidth, scrollViewport
+ )
+
+ fun scissorScrollPort(guiGraphics: GuiGraphics) {
+ guiGraphics.enableScissorWithTranslation(scrollRect())
+ }
+
+ override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean {
+ if (!scrollRect().contains(mouseX, mouseY))
+ return false
+ scrollOffset = (scrollOffset + scrollY * -4)
+ .coerceAtMost(heightEstimate - scrollViewport.toDouble())
+ .coerceAtLeast(.0)
+ .toInt()
+ var offset = height / 2 - scrollViewport / 2 - scrollOffset
+ placedRecipes.forEach {
+ it.moveTo(Point(it.bounds.x, offset))
+ offset += it.bounds.height + gutter
+ }
+ return true
+ }
+
+ override fun renderBackground(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ super.renderBackground(guiGraphics, mouseX, mouseY, partialTick)
+
+ val srect = scrollRect()
+ srect.grow(8, 8)
+ guiGraphics.blitSprite(
+ RenderPipelines.GUI_TEXTURED,
+ CommonTextures.genericWidget(),
+ srect.x, srect.y,
+ srect.width, srect.height
+ )
+
+ scissorScrollPort(guiGraphics)
+ placedRecipes.forEach {
+ guiGraphics.blitSprite(
+ RenderPipelines.GUI_TEXTURED,
+ CommonTextures.genericWidget(),
+ it.bounds.x, it.bounds.y,
+ it.bounds.width, it.bounds.height
+ )
+ }
+ guiGraphics.disableScissor()
+ }
+
+ override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) {
+ scissorScrollPort(guiGraphics)
+ super.render(guiGraphics, mouseX, mouseY, partialTick)
+ guiGraphics.disableScissor()
+ }
+
+ override fun tick() {
+ super.tick()
+ placedRecipes.forEach {
+ it.layoutedRecipe.tick()
+ }
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/RecipeWidget.kt b/src/main/kotlin/features/items/recipes/RecipeWidget.kt
new file mode 100644
index 0000000..f13707c
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/RecipeWidget.kt
@@ -0,0 +1,37 @@
+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.components.Renderable
+import net.minecraft.client.gui.components.events.GuiEventListener
+import net.minecraft.client.gui.narration.NarratableEntry
+import net.minecraft.client.gui.narration.NarrationElementOutput
+import net.minecraft.client.gui.navigation.ScreenRectangle
+import moe.nea.firmament.util.mc.asScreenRectangle
+
+abstract class RecipeWidget : GuiEventListener, Renderable, NarratableEntry {
+ override fun narrationPriority(): NarratableEntry.NarrationPriority? {
+ return NarratableEntry.NarrationPriority.NONE// I am so sorry
+ }
+
+ override fun updateNarration(narrationElementOutput: NarrationElementOutput) {
+ }
+
+ open fun tick() {}
+ private var _focused = false
+ abstract var position: Point
+ abstract val size: Dimension
+ open val rect: Rectangle get() = Rectangle(position, size)
+ override fun setFocused(focused: Boolean) {
+ this._focused = focused
+ }
+
+ override fun isFocused(): Boolean {
+ return this._focused
+ }
+
+ override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean {
+ return rect.contains(mouseX, mouseY)
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/RenderableRecipe.kt b/src/main/kotlin/features/items/recipes/RenderableRecipe.kt
new file mode 100644
index 0000000..20ca17e
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/RenderableRecipe.kt
@@ -0,0 +1,27 @@
+package moe.nea.firmament.features.items.recipes
+
+import java.util.Objects
+import me.shedaniel.math.Rectangle
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.repo.recipes.GenericRecipeRenderer
+
+class RenderableRecipe<T : Any>(
+ val recipe: T,
+ val renderer: GenericRecipeRenderer<T>,
+ val mainItemStack: SBItemStack?,
+) {
+ fun render(bounds: Rectangle): StandaloneRecipeRenderer {
+ val layouter = StandaloneRecipeRenderer(bounds)
+ renderer.render(recipe, bounds, layouter, mainItemStack)
+ return layouter
+ }
+
+// override fun equals(other: Any?): Boolean {
+// if (other !is RenderableRecipe<*>) return false
+// return renderer == other.renderer && recipe == other.recipe
+// }
+//
+// override fun hashCode(): Int {
+// return Objects.hash(recipe, renderer)
+// }
+}
diff --git a/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt b/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt
new file mode 100644
index 0000000..5a834eb
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt
@@ -0,0 +1,77 @@
+package moe.nea.firmament.features.items.recipes
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.client.gui.components.events.AbstractContainerEventHandler
+import net.minecraft.client.gui.components.events.GuiEventListener
+import net.minecraft.network.chat.Component
+import net.minecraft.world.entity.LivingEntity
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.repo.recipes.RecipeLayouter
+
+class StandaloneRecipeRenderer(val bounds: Rectangle) : AbstractContainerEventHandler(), RecipeLayouter {
+
+ fun tick() {
+ widgets.forEach { it.tick() }
+ }
+
+ fun <T : RecipeWidget> addWidget(widget: T): T {
+ this.widgets.add(widget)
+ return widget
+ }
+
+ override fun createCyclingItemSlot(
+ x: Int,
+ y: Int,
+ content: List<SBItemStack>,
+ slotKind: RecipeLayouter.SlotKind
+ ): RecipeLayouter.CyclingItemSlot {
+ return addWidget(ItemSlotWidget(Point(x, y), content, slotKind))
+ }
+
+ val Rectangle.topLeft get() = Point(x, y)
+
+ override fun createTooltip(
+ rectangle: Rectangle,
+ label: List<Component>
+ ) {
+ addWidget(TooltipWidget(rectangle.topLeft, rectangle.size, label))
+ }
+
+ override fun createLabel(
+ x: Int,
+ y: Int,
+ text: Component
+ ): RecipeLayouter.Updater<Component> {
+ return addWidget(ComponentWidget(Point(x, y), text))
+ }
+
+ override fun createArrow(x: Int, y: Int): Rectangle {
+ return addWidget(ArrowWidget(Point(x, y))).rect
+ }
+
+ override fun createMoulConfig(
+ x: Int,
+ y: Int,
+ w: Int,
+ h: Int,
+ component: GuiComponent
+ ) {
+ addWidget(MoulConfigWidget(component, Point(x, y), Dimension(w, h)))
+ }
+
+ override fun createFire(point: Point, animationTicks: Int) {
+ addWidget(FireWidget(point, animationTicks))
+ }
+
+ override fun createEntity(rectangle: Rectangle, entity: LivingEntity) {
+ addWidget(EntityWidget(rectangle.topLeft, rectangle.size, entity))
+ }
+
+ val widgets: MutableList<RecipeWidget> = mutableListOf()
+ override fun children(): List<GuiEventListener> {
+ return widgets
+ }
+}
diff --git a/src/main/kotlin/features/items/recipes/TooltipWidget.kt b/src/main/kotlin/features/items/recipes/TooltipWidget.kt
new file mode 100644
index 0000000..87feb61
--- /dev/null
+++ b/src/main/kotlin/features/items/recipes/TooltipWidget.kt
@@ -0,0 +1,30 @@
+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
+
+class TooltipWidget(
+ override var position: Point,
+ override val size: Dimension,
+ label: List<Component>
+) : RecipeWidget(), RecipeLayouter.Updater<List<Component>> {
+ override fun update(newValue: List<Component>) {
+ this.formattedComponent = newValue.map { it.visualOrderText }
+ }
+
+ var formattedComponent = label.map { it.visualOrderText }
+ override fun render(
+ guiGraphics: GuiGraphics,
+ mouseX: Int,
+ mouseY: Int,
+ partialTick: Float
+ ) {
+ if (rect.contains(mouseX, mouseY))
+ guiGraphics.setTooltipForNextFrame(formattedComponent, mouseX, mouseY)
+ }
+
+}