Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package net.kernelpanicsoft.archie.gui.composables.basic

import androidx.compose.runtime.Composable
import net.kernelpanicsoft.archie.gui.modifiers.Modifier
import net.kernelpanicsoft.archie.gui.theme.ThemeVariants
import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage

/**
* A themed energy-level indicator (looked up in the current theme as `"energy_bar"`), filled
* with a solid color up to `energy / capacity`.
*
* Shares [ProgressBar]'s rendering core but defaults to a bottom-up fill and an energy-flavored
* color, matching how most tech mods orient a power gauge.
*
* @param energy The current stored amount (see [ArchieEnergyStorage.getStoredAmount]).
* @param capacity The maximum capacity (see [ArchieEnergyStorage.getCapacity]); a non-positive
* value renders as empty rather than dividing by zero.
* @param modifier Additional modifiers applied to the outer container.
* @param direction Which edge the fill grows from.
* @param fillColor ARGB color of the filled portion.
* @param variant The theme variant used for the track texture.
*/
@Composable
fun EnergyBar(
energy: Long,
capacity: Long,
modifier: Modifier = Modifier,
direction: ProgressDirection = ProgressDirection.BOTTOM_TO_TOP,
fillColor: Int = 0xFFFF5C33.toInt(),
variant: String = ThemeVariants.DEFAULT,
)
{
val fraction = if (capacity <= 0L) 0f else (energy.toDouble() / capacity.toDouble()).toFloat().coerceIn(0f, 1f)
ThemedFillBar("energy_bar", fraction, modifier, direction, fillColor, variant)
}

/** Convenience overload reading directly from an [ArchieEnergyStorage]. */
@Composable
fun EnergyBar(
storage: ArchieEnergyStorage,
modifier: Modifier = Modifier,
direction: ProgressDirection = ProgressDirection.BOTTOM_TO_TOP,
fillColor: Int = 0xFFFF5C33.toInt(),
variant: String = ThemeVariants.DEFAULT,
) = EnergyBar(storage.getStoredAmount(), storage.getCapacity(), modifier, direction, fillColor, variant)
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package net.kernelpanicsoft.archie.gui.composables.basic

import androidx.compose.runtime.Composable
import dev.architectury.fluid.FluidStack
import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates
import net.kernelpanicsoft.archie.gui.layout.Layout
import net.kernelpanicsoft.archie.gui.layout.MeasureResult
import net.kernelpanicsoft.archie.gui.layout.Renderer
import net.kernelpanicsoft.archie.gui.modifiers.Modifier
import net.kernelpanicsoft.archie.gui.modifiers.sizeIn
import net.kernelpanicsoft.archie.gui.nodes.UINode
import net.kernelpanicsoft.archie.gui.render.AFluidRenderPlatform
import net.kernelpanicsoft.archie.gui.theme.LocalTheme
import net.kernelpanicsoft.archie.gui.theme.ThemeVariants
import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState
import net.kernelpanicsoft.archie.gui.util.extension.invoke
import net.kernelpanicsoft.archie.gui.util.extension.scissor
import net.minecraft.client.gui.GuiGraphics

private const val FLUID_TANK_MIN_WIDTH = 18
private const val FLUID_TANK_MIN_HEIGHT = 54
private const val FLUID_TANK_INSET = 1

/**
* A themed fluid-level indicator (looked up in the current theme as `"fluid_tank"`): a tank
* frame sprite with the real fluid texture and tint (via [AFluidRenderPlatform]) filling it
* bottom-up to `fluid.amount / capacity`.
*
* The fluid sprite is stretched to the tank's interior and clipped with a scissor rather than
* tiled per-block, so it won't repeat at a pixel-perfect 16px grid - a reasonable tradeoff for a
* UI meter over the complexity of manual tiled-quad rendering. See [AFluidRenderPlatform] for
* why this needs a platform bridge at all: Fabric and NeoForge expose a fluid's client
* appearance through unrelated APIs.
*
* @param fluid The fluid and amount to display; an empty stack renders just the tank frame.
* @param capacity The tank's total capacity; a non-positive value renders as empty rather than
* dividing by zero.
* @param modifier Additional modifiers applied to the outer container.
* @param variant The theme variant used for the tank frame texture.
*/
@Composable
fun FluidTank(
fluid: FluidStack,
capacity: Long,
modifier: Modifier = Modifier,
variant: String = ThemeVariants.DEFAULT,
)
{
val theme = LocalTheme.current.getComposableTheme("fluid_tank")
val sizeModifier = Modifier.sizeIn(minWidth = FLUID_TANK_MIN_WIDTH, minHeight = FLUID_TANK_MIN_HEIGHT)

Layout(
name = "FluidTank",
measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} },
modifier = sizeModifier.then(modifier),
renderer = object : Renderer
{
override fun render(
node: UINode, x: Int, y: Int,
guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float,
) = guiGraphics {
val state = theme.getState(TextureStates.DEFAULT, variant)
drawThemeState(state, x, y, node.width, node.height)

if (fluid.isEmpty || capacity <= 0L) return@guiGraphics

val fraction = (fluid.amount.toDouble() / capacity.toDouble()).coerceIn(0.0, 1.0).toFloat()
val sprite = AFluidRenderPlatform.getStillSprite(fluid.fluid) ?: return@guiGraphics

val innerX = x + FLUID_TANK_INSET
val innerY = y + FLUID_TANK_INSET
val innerW = (node.width - FLUID_TANK_INSET * 2).coerceAtLeast(0)
val innerH = (node.height - FLUID_TANK_INSET * 2).coerceAtLeast(0)
val fillH = (innerH * fraction).toInt()
val fillY = innerY + innerH - fillH

if (innerW <= 0 || fillH <= 0) return@guiGraphics

val tint = AFluidRenderPlatform.getTintColor(fluid.fluid)
val a = ((tint ushr 24) and 0xFF) / 255f
val r = ((tint ushr 16) and 0xFF) / 255f
val g = ((tint ushr 8) and 0xFF) / 255f
val b = (tint and 0xFF) / 255f

scissor(innerX, fillY, innerX + innerW, fillY + fillH) {
blit(innerX, innerY, innerW, innerH, 0, sprite, r, g, b, a)
}
}
},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package net.kernelpanicsoft.archie.gui.composables.basic

import androidx.compose.runtime.Composable
import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates
import net.kernelpanicsoft.archie.gui.layout.Layout
import net.kernelpanicsoft.archie.gui.layout.MeasureResult
import net.kernelpanicsoft.archie.gui.layout.Renderer
import net.kernelpanicsoft.archie.gui.modifiers.Modifier
import net.kernelpanicsoft.archie.gui.modifiers.sizeIn
import net.kernelpanicsoft.archie.gui.nodes.UINode
import net.kernelpanicsoft.archie.gui.theme.LocalTheme
import net.kernelpanicsoft.archie.gui.theme.ThemeVariants
import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState
import net.kernelpanicsoft.archie.gui.util.extension.invoke
import net.minecraft.client.gui.GuiGraphics

internal const val FILL_BAR_MIN_WIDTH = 90
internal const val FILL_BAR_MIN_HEIGHT = 16

/** Which edge of a [ProgressBar]/[net.kernelpanicsoft.archie.gui.composables.basic.EnergyBar] the fill grows from. */
enum class ProgressDirection
{
LEFT_TO_RIGHT,
RIGHT_TO_LEFT,
TOP_TO_BOTTOM,
BOTTOM_TO_TOP,
}

/**
* Shared rendering core for [ProgressBar] and [net.kernelpanicsoft.archie.gui.composables.basic.EnergyBar]:
* a themed track sprite (looked up as [themeName] in the current theme) filled with a solid
* color up to [progress].
*/
@Composable
internal fun ThemedFillBar(
themeName: String,
progress: Float,
modifier: Modifier,
direction: ProgressDirection,
fillColor: Int,
variant: String,
)
{
val clamped = progress.coerceIn(0f, 1f)
val theme = LocalTheme.current.getComposableTheme(themeName)
val sizeModifier = Modifier.sizeIn(minWidth = FILL_BAR_MIN_WIDTH, minHeight = FILL_BAR_MIN_HEIGHT)

Layout(
name = themeName,
measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} },
modifier = sizeModifier.then(modifier),
renderer = object : Renderer
{
override fun render(
node: UINode, x: Int, y: Int,
guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float,
) = guiGraphics {
val state = theme.getState(TextureStates.DEFAULT, variant)
drawThemeState(state, x, y, node.width, node.height)

var fx = x
var fy = y
var fw = node.width
var fh = node.height
when (direction)
{
ProgressDirection.LEFT_TO_RIGHT -> fw = (node.width * clamped).toInt()
ProgressDirection.RIGHT_TO_LEFT ->
{
fw = (node.width * clamped).toInt()
fx = x + node.width - fw
}

ProgressDirection.TOP_TO_BOTTOM -> fh = (node.height * clamped).toInt()
ProgressDirection.BOTTOM_TO_TOP ->
{
fh = (node.height * clamped).toInt()
fy = y + node.height - fh
}
}
if (fw > 0 && fh > 0) fill(fx, fy, fx + fw, fy + fh, fillColor)
}
},
)
}

/**
* A themed linear progress indicator: an empty-track sprite from the current theme (looked up as
* `"progress_bar"`), filled with a solid color up to [progress].
*
* There's no built-in animation or recomposition trigger here - drive [progress] from an
* observed block entity field (see [net.kernelpanicsoft.archie.gui.blockentity.observeProperty])
* for a live machine-processing indicator.
*
* @param progress Fraction complete, clamped to `0f..1f`.
* @param modifier Additional modifiers applied to the outer container.
* @param direction Which edge the fill grows from.
* @param fillColor ARGB color of the filled portion.
* @param variant The theme variant used for the track texture.
*/
@Composable
fun ProgressBar(
progress: Float,
modifier: Modifier = Modifier,
direction: ProgressDirection = ProgressDirection.LEFT_TO_RIGHT,
fillColor: Int = 0xFF6BA8FF.toInt(),
variant: String = ThemeVariants.DEFAULT,
) = ThemedFillBar("progress_bar", progress, modifier, direction, fillColor, variant)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.kernelpanicsoft.archie.gui.render

import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.world.level.material.Fluid

/**
* Cross-loader lookup of a [Fluid]'s client-rendering appearance, backed by an `actual` per mod
* loader - Fabric's `FluidRenderHandlerRegistry` and NeoForge's `IClientFluidTypeExtensions`
* expose the same information through unrelated APIs, so [net.kernelpanicsoft.archie.gui.composables.basic.FluidTank]
* goes through this instead of touching either directly.
*
* Client-only; only ever called from GUI rendering code.
*/
expect object AFluidRenderPlatform
{
/** The fluid's still-texture sprite from the blocks atlas, or `null` if it can't be resolved. */
fun getStillSprite(fluid: Fluid): TextureAtlasSprite?

/** The ARGB tint color applied over [getStillSprite]'s sprite (`0xFFFFFFFF` = no tint). */
fun getTintColor(fluid: Fluid): Int
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package net.kernelpanicsoft.archie.serialization

import net.kernelpanicsoft.archie.config.toSnakeCase
import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage
import net.kernelpanicsoft.archie.transfer.ArchieFluidStorage
import net.kernelpanicsoft.archie.transfer.ArchieItemStorage
import dev.architectury.fluid.FluidStack
import kotlinx.serialization.KSerializer
Expand All @@ -27,6 +29,8 @@ class FluidStackNBTHolderImpl(private val stack: FluidStack) : NBTHolder
{
private val data: MutableMap<String, NbtTag> = mutableMapOf()
private val itemStorage: MutableMap<String, ArchieItemStorage> = mutableMapOf()
private val fluidStorage: MutableMap<String, ArchieFluidStorage> = mutableMapOf()
private val energyStorage: MutableMap<String, ArchieEnergyStorage> = mutableMapOf()

init
{
Expand Down Expand Up @@ -149,6 +153,28 @@ class FluidStackNBTHolderImpl(private val stack: FluidStack) : NBTHolder
}
}

override fun fluidField(limit: Long, size: Int): PropertyDelegateProvider<Any?, ReadOnlyProperty<Any?, ArchieFluidStorage>>
{
return PropertyDelegateProvider { thisRef, property ->
val onUpdate = {
saveToStack()
}
fluidStorage[property.name.toSnakeCase()] = ArchieFluidStorage(limit, size, onUpdate)
ReadOnlyProperty { _, _ -> fluidStorage[property.name.toSnakeCase()]!! }
}
}

override fun energyField(capacity: Long): PropertyDelegateProvider<Any?, ReadOnlyProperty<Any?, ArchieEnergyStorage>>
{
return PropertyDelegateProvider { thisRef, property ->
val onUpdate = {
saveToStack()
}
energyStorage[property.name.toSnakeCase()] = ArchieEnergyStorage(capacity, onUpdate)
ReadOnlyProperty { _, _ -> energyStorage[property.name.toSnakeCase()]!! }
}
}

override fun loadFromTag(compoundTag: CompoundTag)
{
forEachTag(compoundTag) { (key, value) ->
Expand All @@ -159,6 +185,16 @@ class FluidStackNBTHolderImpl(private val stack: FluidStack) : NBTHolder
value.createSnapshot()
})
}
fluidStorage.forEach { (key, value) ->
value.readSnapshot(data.getOrPut(key) {
value.createSnapshot()
})
}
energyStorage.forEach { (key, value) ->
value.readSnapshot(data.getOrPut(key) {
value.createSnapshot()
})
}
}

override fun saveToTag(compoundTag: CompoundTag)
Expand All @@ -167,6 +203,12 @@ class FluidStackNBTHolderImpl(private val stack: FluidStack) : NBTHolder
itemStorage.forEach { (key, value) ->
data[key] = value.createSnapshot()
}
fluidStorage.forEach { (key, value) ->
data[key] = value.createSnapshot()
}
energyStorage.forEach { (key, value) ->
data[key] = value.createSnapshot()
}
data.forEach { (key, value) ->
put(key, value)

Expand Down
Loading