diff options
Diffstat (limited to 'src/main/kotlin/features/items/recipes')
13 files changed, 1022 insertions, 0 deletions
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) + } + +} |
