Add interactive storage overlay

This commit is contained in:
Linnea Gräf
2024-07-10 01:34:37 +02:00
parent 4e1cda1d64
commit 986ce538f1
39 changed files with 1141 additions and 228 deletions

View File

@@ -15,7 +15,7 @@ import net.minecraft.text.Text
import moe.nea.firmament.apis.UrsaManager
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.features.inventory.buttons.InventoryButtons
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverviewScreen
import moe.nea.firmament.features.world.FairySouls
import moe.nea.firmament.gui.config.AllConfigsGui
import moe.nea.firmament.gui.config.BooleanHandler
@@ -107,7 +107,7 @@ fun firmamentCommand() = literal("firmament") {
}
thenLiteral("storage") {
thenExecute {
ScreenUtil.setScreenLater(StorageOverlayScreen())
ScreenUtil.setScreenLater(StorageOverviewScreen())
MC.player?.networkHandler?.sendChatCommand("storage")
}
}

View File

@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -9,5 +10,6 @@ package moe.nea.firmament.events
import net.minecraft.client.gui.screen.Screen
data class ScreenChangeEvent(val old: Screen?, val new: Screen?) : FirmamentEvent.Cancellable() {
var overrideScreen: Screen? = null
companion object : FirmamentEventBus<ScreenChangeEvent>()
}

View File

@@ -15,7 +15,7 @@ interface SubscriptionOwner {
}
data class Subscription<T : FirmamentEvent>(
val owner: SubscriptionOwner,
val owner: Any,
val invoke: (T) -> Unit,
val eventBus: FirmamentEventBus<T>,
)

View File

@@ -98,11 +98,7 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
}
private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) {
if (it.owner.delegateFeature in features.values) { // TODO: better check here, somehow. probably implement some interface method
it.eventBus.subscribe(false, it.invoke) // TODO: pass through receivesCancelled from the annotation
} else {
Firmament.logger.error("Ignoring event listener for ${it.eventBus} in ${it.owner}")
}
it.eventBus.subscribe(false, it.invoke)
}
fun loadFeature(feature: FirmamentFeature) {

View File

@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -21,11 +22,6 @@ sealed interface StorageBackingHandle {
val handler: GenericContainerScreenHandler
}
/**
* No open "server side" screen.
*/
object None : StorageBackingHandle
/**
* The main storage overview is open. Clicking on a slot will open that page. This page is accessible via `/storage`
*/
@@ -48,7 +44,7 @@ sealed interface StorageBackingHandle {
* selection screen.
*/
fun fromScreen(screen: Screen?): StorageBackingHandle? {
if (screen == null) return None
if (screen == null) return null
if (screen !is GenericContainerScreen) return null
val title = screen.title.unformattedString
if (title == "Storage") return Overview(screen.screenHandler)

View File

@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -9,12 +10,14 @@ package moe.nea.firmament.features.inventory.storageoverlay
import java.util.SortedMap
import kotlinx.serialization.serializer
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
import net.minecraft.client.gui.screen.ingame.HandledScreen
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.ScreenUtil.setScreenLater
import moe.nea.firmament.util.customgui.customGui
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
object StorageOverlay : FirmamentFeature {
@@ -33,49 +36,41 @@ object StorageOverlay : FirmamentFeature {
val margin by integer("margin", 1, 60) { 20 }
}
fun adjustScrollSpeed(amount: Double): Double {
return amount * TConfig.scrollSpeed * (if (TConfig.inverseScroll) 1 else -1)
}
override val config: TConfig
get() = TConfig
var lastStorageOverlay: Screen? = null
var shouldReturnToStorageOverlayFrom: Screen? = null
var shouldReturnToStorageOverlay: Screen? = null
var currentHandler: StorageBackingHandle? = StorageBackingHandle.None
var currentHandler: StorageBackingHandle? = null
@Subscribe
fun onTick(event: TickEvent) {
rememberContent(currentHandler ?: return)
}
@Subscribe
fun onScreenChangeLegacy(event: ScreenChangeEvent) {
currentHandler = StorageBackingHandle.fromScreen(event.new)
if (event.old is StorageOverlayScreen && !event.old.isClosing) {
event.old.setHandler(currentHandler)
if (currentHandler != null)
// TODO: Consider instead only replacing rendering? might make a lot of stack handling easier
event.cancel()
}
}
@Subscribe
fun onScreenChange(it: ScreenChangeEvent) {
if (lastStorageOverlay != null && it.new != null) {
shouldReturnToStorageOverlay = lastStorageOverlay
shouldReturnToStorageOverlayFrom = it.new
lastStorageOverlay = null
} else if (it.old === shouldReturnToStorageOverlayFrom) {
if (shouldReturnToStorageOverlay != null && it.new == null)
setScreenLater(shouldReturnToStorageOverlay)
shouldReturnToStorageOverlay = null
shouldReturnToStorageOverlayFrom = null
val storageOverlayScreen = it.old as? StorageOverlayScreen
?: ((it.old as? HandledScreen<*>)?.customGui as? StorageOverlayCustom)?.overview
if (it.new == null && storageOverlayScreen != null && !storageOverlayScreen.isExiting) {
it.overrideScreen = storageOverlayScreen
return
}
val screen = it.new as? GenericContainerScreen ?: return
currentHandler = StorageBackingHandle.fromScreen(screen)
screen.customGui = StorageOverlayCustom(
currentHandler as? StorageBackingHandle.Page ?: return,
screen,
storageOverlayScreen ?: return)
}
private fun rememberContent(handler: StorageBackingHandle) {
fun rememberContent(handler: StorageBackingHandle) {
// TODO: Make all of these functions work on deltas / updates instead of the entire contents
val data = Data.data?.storageInventories ?: return
when (handler) {
StorageBackingHandle.None -> {}
is StorageBackingHandle.Overview -> rememberStorageOverview(handler, data)
is StorageBackingHandle.Page -> rememberPage(handler, data)
}
@@ -90,7 +85,7 @@ object StorageOverlay : FirmamentFeature {
// Ignore unloaded item stacks
if (stack.isEmpty) continue
val slot = StoragePageSlot.fromOverviewSlotIndex(index) ?: continue
val isEmpty = stack.item in StorageOverlayScreen.emptyStorageSlotItems
val isEmpty = stack.item in StorageOverviewScreen.emptyStorageSlotItems
if (slot in data) {
if (isEmpty)
data.remove(slot)

View File

@@ -0,0 +1,97 @@
/*
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.features.inventory.storageoverlay
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.screen.slot.Slot
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.customgui.CustomGui
class StorageOverlayCustom(
val handler: StorageBackingHandle.Page,
val screen: GenericContainerScreen,
val overview: StorageOverlayScreen,
) : CustomGui() {
override fun onVoluntaryExit(): Boolean {
overview.isExiting = true
return super.onVoluntaryExit()
}
override fun getBounds(): List<Rectangle> {
return overview.getBounds()
}
override fun afterSlotRender(context: DrawContext, slot: Slot) {
if (slot.inventory !is PlayerInventory)
context.disableScissor()
}
override fun beforeSlotRender(context: DrawContext, slot: Slot) {
if (slot.inventory !is PlayerInventory)
overview.createScissors(context)
}
override fun onInit() {
overview.init(MinecraftClient.getInstance(), screen.width, screen.height)
}
override fun isPointOverSlot(slot: Slot, xOffset: Int, yOffset: Int, pointX: Double, pointY: Double): Boolean {
if (!super.isPointOverSlot(slot, xOffset, yOffset, pointX, pointY))
return false
if (slot.inventory !is PlayerInventory) {
if (!overview.getScrollPanelInner().contains(pointX, pointY))
return false
}
return true
}
override fun shouldDrawForeground(): Boolean {
return false
}
override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean {
return overview.mouseClicked(mouseX, mouseY, button, handler.storagePageSlot)
}
override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) {
overview.drawBackgrounds(drawContext)
overview.drawPages(drawContext,
mouseX,
mouseY,
delta,
handler.storagePageSlot,
screen.screenHandler.slots.take(screen.screenHandler.rows * 9).drop(9),
Point((screen as AccessorHandledScreen).x_Firmament, screen.y_Firmament))
overview.drawScrollBar(drawContext)
}
override fun moveSlot(slot: Slot) {
val index = slot.index
if (index in 0..<36) {
val (x, y) = overview.getPlayerInventorySlotPosition(index)
slot.x = x - (screen as AccessorHandledScreen).x_Firmament
slot.y = y - screen.y_Firmament
} else {
slot.x = -100000
slot.y = -100000
}
}
override fun mouseScrolled(
mouseX: Double,
mouseY: Double,
horizontalAmount: Double,
verticalAmount: Double
): Boolean {
return overview.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
}

View File

@@ -1,106 +1,75 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.features.inventory.storageoverlay
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.assertNotNullOr
import moe.nea.firmament.util.toShedaniel
import net.minecraft.block.Blocks
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
import net.minecraft.item.Item
import net.minecraft.item.Items
import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket
import net.minecraft.screen.slot.Slot
import net.minecraft.text.Text
import net.minecraft.util.DyeColor
import kotlin.math.max
import net.minecraft.util.Identifier
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.ScreenUtil
import moe.nea.firmament.util.assertTrueOr
class StorageOverlayScreen : Screen(Text.literal("")) {
class StorageOverlayScreen() : Screen(Text.empty()) {
companion object {
val emptyStorageSlotItems = listOf<Item>(
Blocks.RED_STAINED_GLASS_PANE.asItem(),
Blocks.BROWN_STAINED_GLASS_PANE.asItem(),
Items.GRAY_DYE
)
val pageWidth get() = 19 * 9
}
val PLAYER_WIDTH = 184
val PLAYER_HEIGHT = 91
val PLAYER_Y_INSET = 3
val SLOT_SIZE = 18
val PADDING = 10
val PAGE_WIDTH = SLOT_SIZE * 9
val HOTBAR_X = 12
val HOTBAR_Y = 67
val MAIN_INVENTORY_Y = 9
val SCROLL_BAR_WIDTH = 8
val SCROLL_BAR_HEIGHT = 16
private var handler: StorageBackingHandle = StorageBackingHandle.None
val content = StorageOverlay.Data.data ?: StorageData()
var isClosing = false
private fun discardOldHandle() {
val player = assertNotNullOr(MC.player) { return }
val handle = this.handler
if (handle is StorageBackingHandle.HasBackingScreen) {
player.networkHandler.sendPacket(CloseHandledScreenC2SPacket(handle.handler.syncId))
if (player.currentScreenHandler === handle.handler) {
player.currentScreenHandler = player.playerScreenHandler
@Subscribe
fun onCommand(event: CommandEvent.SubCommand) {
event.subcommand("teststorage") {
executes {
ScreenUtil.setScreenLater(StorageOverlayScreen())
MC.sendCommand("ec")
0
}
}
}
}
fun setHandler(handler: StorageBackingHandle?) {
discardOldHandle()
if (handler != null)
this.handler = handler
var isExiting: Boolean = false
var scroll: Float = 0F
var pageWidthCount = StorageOverlay.TConfig.rows
inner class Measurements {
val innerScrollPanelWidth = PAGE_WIDTH * pageWidthCount + (pageWidthCount - 1) * PADDING
val overviewWidth = innerScrollPanelWidth + 3 * PADDING + SCROLL_BAR_WIDTH
val x = width / 2 - overviewWidth / 2
val overviewHeight = minOf(3 * 18 * 6, height - PLAYER_HEIGHT - minOf(80, height / 10))
val innerScrollPanelHeight = overviewHeight - PADDING * 2
val y = height / 2 - (overviewHeight + PLAYER_HEIGHT) / 2
val playerX = width / 2 - PLAYER_WIDTH / 2
val playerY = y + overviewHeight - PLAYER_Y_INSET
}
var scroll = 0
var lastRenderedHeight = 0
var measurements = Measurements()
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
context.fill(0, 0, width, height, 0x90000000.toInt())
layoutedForEach { (key, value), offsetX, offsetY ->
context.matrices.push()
context.matrices.translate(offsetX.toFloat(), offsetY.toFloat(), 0F)
renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY)
context.matrices.pop()
}
}
inline fun layoutedForEach(onEach: (data: Pair<StoragePageSlot, StorageData.StorageInventory>, offsetX: Int, offsetY: Int) -> Unit) {
var offsetY = 0
var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll
var totalHeight = -currentMaxHeight
content.storageInventories.onEachIndexed { index, (key, value) ->
val pageX = (index % StorageOverlay.config.rows)
if (pageX == 0) {
currentMaxHeight += StorageOverlay.config.padding
offsetY += currentMaxHeight
totalHeight += currentMaxHeight
currentMaxHeight = 0
}
val xPosition =
width / 2 - (StorageOverlay.config.rows * (pageWidth + StorageOverlay.config.padding) - StorageOverlay.config.padding) / 2 + pageX * (pageWidth + StorageOverlay.config.padding)
onEach(Pair(key, value), xPosition, offsetY)
val height = getStorePageHeight(value)
currentMaxHeight = max(currentMaxHeight, height)
}
lastRenderedHeight = totalHeight + currentMaxHeight
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
layoutedForEach { (k, p), x, y ->
val rx = mouseX - x
val ry = mouseY - y
if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) {
close()
StorageOverlay.lastStorageOverlay = this
k.navigateTo()
return true
}
}
return super.mouseClicked(mouseX, mouseY, button)
}
fun getStorePageHeight(page: StorageData.StorageInventory): Int {
return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60
var lastRenderedInnerHeight = 0
override fun init() {
super.init()
pageWidthCount = StorageOverlay.TConfig.rows
.coerceAtMost((width - PADDING) / (PAGE_WIDTH + PADDING))
.coerceAtLeast(1)
measurements = Measurements()
}
override fun mouseScrolled(
@@ -109,39 +78,233 @@ class StorageOverlayScreen() : Screen(Text.empty()) {
horizontalAmount: Double,
verticalAmount: Double
): Boolean {
scroll =
(scroll + verticalAmount * StorageOverlay.config.scrollSpeed *
(if (StorageOverlay.config.inverseScroll) 1 else -1)).toInt()
.coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0)
scroll = (scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toFloat()
.coerceAtMost(getMaxScroll())
.coerceAtLeast(0F)
return true
}
private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) {
context.drawText(MC.font, page.title, 2, 2, -1, true)
val inventory = page.inventory
if (inventory == null) {
// TODO: Missing texture
context.fill(0, 0, pageWidth, 60, DyeColor.RED.toShedaniel().darker(4.0).color)
context.drawCenteredTextWithShadow(MC.font, Text.literal("Not loaded yet"), pageWidth / 2, 30, -1)
return
}
fun getMaxScroll() = lastRenderedInnerHeight.toFloat() - getScrollPanelInner().height
for ((index, stack) in inventory.stacks.withIndex()) {
val x = (index % 9) * 19
val y = (index / 9) * 19 + MC.font.fontHeight + 2
if (((mouseX - x) in 0 until 18) && ((mouseY - y) in 0 until 18)) {
context.fill(x, y, x + 18, y + 18, 0x80808080.toInt())
} else {
context.fill(x, y, x + 18, y + 18, 0x40808080.toInt())
}
context.drawItem(stack, x + 1, y + 1)
context.drawItemInSlot(MC.font, stack, x + 1, y + 1)
}
}
val playerInventorySprite = Identifier.of("firmament:storageoverlay/player_inventory")
val upperBackgroundSprite = Identifier.of("firmament:storageoverlay/upper_background")
val slotRowSprite = Identifier.of("firmament:storageoverlay/storage_row")
val scrollbarBackground = Identifier.of("firmament:storageoverlay/scroll_bar_background")
val scrollbarKnob = Identifier.of("firmament:storageoverlay/scroll_bar_knob")
override fun close() {
discardOldHandle()
isClosing = true
isExiting = true
super.close()
}
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
drawBackgrounds(context)
drawPages(context, mouseX, mouseY, delta, null, null, Point())
drawScrollBar(context)
drawPlayerInventory(context, mouseX, mouseY, delta)
}
fun getScrollbarPercentage(): Float {
return scroll / getMaxScroll()
}
fun drawScrollBar(context: DrawContext) {
val sbRect = getScrollBarRect()
context.drawGuiTexture(
scrollbarBackground,
sbRect.minX, sbRect.minY,
sbRect.width, sbRect.height,
)
context.drawGuiTexture(
scrollbarKnob,
sbRect.minX, sbRect.minY + (getScrollbarPercentage() * (sbRect.height - SCROLL_BAR_HEIGHT)).toInt(),
SCROLL_BAR_WIDTH, SCROLL_BAR_HEIGHT
)
}
fun drawBackgrounds(context: DrawContext) {
context.drawGuiTexture(upperBackgroundSprite,
measurements.x,
measurements.y,
0,
measurements.overviewWidth,
measurements.overviewHeight)
context.drawGuiTexture(playerInventorySprite,
measurements.playerX,
measurements.playerY,
0,
PLAYER_WIDTH,
PLAYER_HEIGHT)
}
fun getPlayerInventorySlotPosition(int: Int): Pair<Int, Int> {
if (int < 9) {
return Pair(measurements.playerX + int * SLOT_SIZE + HOTBAR_X, HOTBAR_Y + measurements.playerY)
}
return Pair(
measurements.playerX + (int % 9) * SLOT_SIZE + HOTBAR_X,
measurements.playerY + (int / 9 - 1) * SLOT_SIZE + MAIN_INVENTORY_Y
)
}
fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
val items = MC.player?.inventory?.main ?: return
items.withIndex().forEach { (index, item) ->
val (x, y) = getPlayerInventorySlotPosition(index)
context.drawItem(item, x, y, 0)
context.drawItemInSlot(textRenderer, item, x, y)
}
}
fun getScrollBarRect(): Rectangle {
return Rectangle(measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING,
measurements.y + PADDING,
SCROLL_BAR_WIDTH,
measurements.innerScrollPanelHeight)
}
fun getScrollPanelInner(): Rectangle {
return Rectangle(measurements.x + PADDING,
measurements.y + PADDING,
measurements.innerScrollPanelWidth,
measurements.innerScrollPanelHeight)
}
fun createScissors(context: DrawContext) {
val rect = getScrollPanelInner()
context.enableScissor(
rect.minX, rect.minY,
rect.maxX, rect.maxY
)
}
fun drawPages(
context: DrawContext, mouseX: Int, mouseY: Int, delta: Float,
excluding: StoragePageSlot?,
slots: List<Slot>?,
slotOffset: Point
) {
createScissors(context)
val data = StorageOverlay.Data.data ?: StorageData()
layoutedForEach(data) { rect, page, inventory ->
drawPage(context,
rect.x,
rect.y,
page, inventory,
if (excluding == page) slots else null,
slotOffset
)
}
context.disableScissor()
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
return mouseClicked(mouseX, mouseY, button, null)
}
fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean {
if (getScrollPanelInner().contains(mouseX, mouseY)) {
val data = StorageOverlay.Data.data ?: StorageData()
layoutedForEach(data) { rect, page, _ ->
if (rect.contains(mouseX, mouseY) && activePage != page && button == 0) {
page.navigateTo()
return true
}
}
return false
}
val sbRect = getScrollBarRect()
if (sbRect.contains(mouseX, mouseY)) {
// TODO: support dragging of the mouse and such
val percentage = (mouseY - sbRect.getY()) / sbRect.getHeight()
scroll = (getMaxScroll() * percentage).toFloat()
mouseScrolled(0.0, 0.0, 0.0, 0.0)
return true
}
return false
}
private inline fun layoutedForEach(
data: StorageData,
func: (
rectangle: Rectangle,
page: StoragePageSlot, inventory: StorageData.StorageInventory,
) -> Unit
) {
var yOffset = -scroll.toInt()
var xOffset = 0
var maxHeight = 0
for ((page, inventory) in data.storageInventories.entries) {
val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 4 + textRenderer.fontHeight }
?: 18
maxHeight = maxOf(maxHeight, currentHeight)
val rect = Rectangle(
measurements.x + PADDING + (PAGE_WIDTH + PADDING) * xOffset,
yOffset + measurements.y + PADDING,
PAGE_WIDTH,
currentHeight
)
func(rect, page, inventory)
xOffset++
if (xOffset >= pageWidthCount) {
yOffset += maxHeight
xOffset = 0
maxHeight = 0
}
}
lastRenderedInnerHeight = maxHeight + yOffset + scroll.toInt()
}
fun drawPage(
context: DrawContext,
x: Int,
y: Int,
page: StoragePageSlot,
inventory: StorageData.StorageInventory,
slots: List<Slot>?,
slotOffset: Point,
): Int {
val inv = inventory.inventory
if (inv == null) {
context.drawGuiTexture(upperBackgroundSprite, x, y, PAGE_WIDTH, 18)
context.drawText(textRenderer,
Text.literal("TODO: open this page"),
x + 4,
y + 4,
-1,
true)
return 18
}
assertTrueOr(slots == null || slots.size == inv.stacks.size) { return 0 }
val name = page.defaultName()
context.drawText(textRenderer, Text.literal(name), x + 4, y + 2,
if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true)
context.drawGuiTexture(slotRowSprite, x, y + 4 + textRenderer.fontHeight, PAGE_WIDTH, inv.rows * SLOT_SIZE)
inv.stacks.forEachIndexed { index, stack ->
val slotX = (index % 9) * SLOT_SIZE + x + 1
val slotY = (index / 9) * SLOT_SIZE + y + 4 + textRenderer.fontHeight + 1
if (slots == null) {
context.drawItem(stack, slotX, slotY)
context.drawItemInSlot(textRenderer, stack, slotX, slotY)
} else {
val slot = slots[index]
slot.x = slotX - slotOffset.x
slot.y = slotY - slotOffset.y
}
}
return inv.rows * SLOT_SIZE + 4 + textRenderer.fontHeight
}
fun getBounds(): List<Rectangle> {
return listOf(
Rectangle(measurements.x,
measurements.y,
measurements.overviewWidth,
measurements.overviewHeight),
Rectangle(measurements.playerX,
measurements.playerY,
PLAYER_WIDTH,
PLAYER_HEIGHT))
}
}

View File

@@ -0,0 +1,126 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.features.inventory.storageoverlay
import kotlin.math.max
import net.minecraft.block.Blocks
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
import net.minecraft.item.Item
import net.minecraft.item.Items
import net.minecraft.text.Text
import net.minecraft.util.DyeColor
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.toShedaniel
class StorageOverviewScreen() : Screen(Text.empty()) {
companion object {
val emptyStorageSlotItems = listOf<Item>(
Blocks.RED_STAINED_GLASS_PANE.asItem(),
Blocks.BROWN_STAINED_GLASS_PANE.asItem(),
Items.GRAY_DYE
)
val pageWidth get() = 19 * 9
}
val content = StorageOverlay.Data.data ?: StorageData()
var isClosing = false
var scroll = 0
var lastRenderedHeight = 0
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
context.fill(0, 0, width, height, 0x90000000.toInt())
layoutedForEach { (key, value), offsetX, offsetY ->
context.matrices.push()
context.matrices.translate(offsetX.toFloat(), offsetY.toFloat(), 0F)
renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY)
context.matrices.pop()
}
}
inline fun layoutedForEach(onEach: (data: Pair<StoragePageSlot, StorageData.StorageInventory>, offsetX: Int, offsetY: Int) -> Unit) {
var offsetY = 0
var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll
var totalHeight = -currentMaxHeight
content.storageInventories.onEachIndexed { index, (key, value) ->
val pageX = (index % StorageOverlay.config.rows)
if (pageX == 0) {
currentMaxHeight += StorageOverlay.config.padding
offsetY += currentMaxHeight
totalHeight += currentMaxHeight
currentMaxHeight = 0
}
val xPosition =
width / 2 - (StorageOverlay.config.rows * (pageWidth + StorageOverlay.config.padding) - StorageOverlay.config.padding) / 2 + pageX * (pageWidth + StorageOverlay.config.padding)
onEach(Pair(key, value), xPosition, offsetY)
val height = getStorePageHeight(value)
currentMaxHeight = max(currentMaxHeight, height)
}
lastRenderedHeight = totalHeight + currentMaxHeight
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
layoutedForEach { (k, p), x, y ->
val rx = mouseX - x
val ry = mouseY - y
if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) {
close()
StorageOverlay.lastStorageOverlay = this
k.navigateTo()
return true
}
}
return super.mouseClicked(mouseX, mouseY, button)
}
fun getStorePageHeight(page: StorageData.StorageInventory): Int {
return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60
}
override fun mouseScrolled(
mouseX: Double,
mouseY: Double,
horizontalAmount: Double,
verticalAmount: Double
): Boolean {
scroll =
(scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toInt()
.coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0)
return true
}
private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) {
context.drawText(MC.font, page.title, 2, 2, -1, true)
val inventory = page.inventory
if (inventory == null) {
// TODO: Missing texture
context.fill(0, 0, pageWidth, 60, DyeColor.RED.toShedaniel().darker(4.0).color)
context.drawCenteredTextWithShadow(MC.font, Text.literal("Not loaded yet"), pageWidth / 2, 30, -1)
return
}
for ((index, stack) in inventory.stacks.withIndex()) {
val x = (index % 9) * 19
val y = (index / 9) * 19 + MC.font.fontHeight + 2
if (((mouseX - x) in 0 until 18) && ((mouseY - y) in 0 until 18)) {
context.fill(x, y, x + 18, y + 18, 0x80808080.toInt())
} else {
context.fill(x, y, x + 18, y + 18, 0x40808080.toInt())
}
context.drawItem(stack, x + 1, y + 1)
context.drawItemInSlot(MC.font, stack, x + 1, y + 1)
}
}
override fun close() {
isClosing = true
super.close()
}
}

View File

@@ -19,20 +19,21 @@ import me.shedaniel.rei.api.client.registry.transfer.TransferHandlerRegistry
import me.shedaniel.rei.api.common.entry.EntryStack
import me.shedaniel.rei.api.common.entry.type.EntryTypeRegistry
import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes
import moe.nea.firmament.events.HandledScreenPushREIEvent
import moe.nea.firmament.features.inventory.CraftingOverlay
import moe.nea.firmament.rei.recipes.SBCraftingRecipe
import moe.nea.firmament.rei.recipes.SBForgeRecipe
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.skyblockId
import moe.nea.firmament.util.unformattedString
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.item.ItemStack
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import moe.nea.firmament.events.HandledScreenPushREIEvent
import moe.nea.firmament.features.inventory.CraftingOverlay
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
import moe.nea.firmament.rei.recipes.SBCraftingRecipe
import moe.nea.firmament.rei.recipes.SBForgeRecipe
import moe.nea.firmament.rei.recipes.SBMobDropRecipe
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.skyblockId
import moe.nea.firmament.util.unformattedString
class FirmamentReiPlugin : REIClientPlugin {
@@ -44,6 +45,7 @@ class FirmamentReiPlugin : REIClientPlugin {
val SKYBLOCK_ITEM_TYPE_ID = Identifier.of("firmament", "skyblockitems")
}
override fun registerTransferHandlers(registry: TransferHandlerRegistry) {
registry.register(TransferHandler { context ->
val screen = context.containerScreen
@@ -69,6 +71,7 @@ class FirmamentReiPlugin : REIClientPlugin {
override fun registerExclusionZones(zones: ExclusionZones) {
zones.register(HandledScreen::class.java) { HandledScreenPushREIEvent.publish(HandledScreenPushREIEvent(it)).rectangles }
zones.register(StorageOverlayScreen::class.java) { it.getBounds() }
}
override fun registerDisplays(registry: DisplayRegistry) {
@@ -80,7 +83,8 @@ class FirmamentReiPlugin : REIClientPlugin {
SBForgeRecipe.Category.categoryIdentifier,
SkyblockForgeRecipeDynamicGenerator
)
registry.registerDisplayGenerator(SBMobDropRecipe.Category.categoryIdentifier, SkyblockMobDropRecipeDynamicGenerator)
registry.registerDisplayGenerator(SBMobDropRecipe.Category.categoryIdentifier,
SkyblockMobDropRecipeDynamicGenerator)
}
override fun registerCollapsibleEntries(registry: CollapsibleEntryRegistry) {

View File

@@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.util.customgui
import net.minecraft.screen.slot.Slot
interface CoordRememberingSlot {
fun rememberCoords_firmament()
fun restoreCoords_firmament()
fun getOriginalX_firmament(): Int
fun getOriginalY_firmament(): Int
}
val Slot.originalX get() = (this as CoordRememberingSlot).getOriginalX_firmament()
val Slot.originalY get() = (this as CoordRememberingSlot).getOriginalY_firmament()

View File

@@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.util.customgui
import me.shedaniel.math.Rectangle
import net.minecraft.client.gui.DrawContext
import net.minecraft.screen.slot.Slot
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenPushREIEvent
abstract class CustomGui {
abstract fun getBounds(): List<Rectangle>
open fun moveSlot(slot: Slot) {
// TODO: return a Pair maybe? worth an investigation
}
companion object {
@Subscribe
fun onExclusionZone(event: HandledScreenPushREIEvent) {
val customGui = event.screen.customGui ?: return
event.rectangles.addAll(customGui.getBounds())
}
}
open fun render(
drawContext: DrawContext,
delta: Float,
mouseX: Int,
mouseY: Int
) {
}
open fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean {
return false
}
open fun afterSlotRender(context: DrawContext, slot: Slot) {}
open fun beforeSlotRender(context: DrawContext, slot: Slot) {}
open fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
return false
}
open fun isClickOutsideBounds(mouseX: Double, mouseY: Double): Boolean {
return getBounds().none { it.contains(mouseX, mouseY) }
}
open fun isPointWithinBounds(
x: Int,
y: Int,
width: Int,
height: Int,
pointX: Double,
pointY: Double,
): Boolean {
return getBounds().any { it.contains(pointX, pointY) } &&
Rectangle(x, y, width, height).contains(pointX, pointY)
}
open fun isPointOverSlot(slot: Slot, xOffset: Int, yOffset: Int, pointX: Double, pointY: Double): Boolean {
return isPointWithinBounds(slot.x + xOffset, slot.y + yOffset, 16, 16, pointX, pointY)
}
open fun onInit() {}
open fun shouldDrawForeground(): Boolean {
return true
}
open fun onVoluntaryExit(): Boolean {
return true
}
}

View File

@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.util.customgui
import net.minecraft.client.gui.screen.ingame.HandledScreen
@Suppress("FunctionName")
interface HasCustomGui {
fun getCustomGui_Firmament(): CustomGui?
fun setCustomGui_Firmament(gui: CustomGui?)
}
var <T : HandledScreen<*>> T.customGui: CustomGui?
get() = (this as HasCustomGui).getCustomGui_Firmament()
set(value) {
(this as HasCustomGui).setCustomGui_Firmament(value)
}