From 6cb637209f785ce8950bfe9334e2c635d6a20874 Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Tue, 4 Aug 2026 12:15:51 -0400 Subject: [PATCH 1/2] Add ProgressBar/EnergyBar/FluidTank composables and energy storage New GUI composables (all under gui/composables/basic): - ProgressBar: themed linear fill indicator, four fill directions. - EnergyBar: ProgressBar tuned for ArchieEnergyStorage (bottom-up default). - FluidTank: themed tank indicator rendering the real fluid texture and tint via a new per-loader render bridge (AFluidRenderPlatform), since Fabric's FluidRenderHandlerRegistry and NeoForge's IClientFluidTypeExtensions expose a fluid's client appearance through unrelated APIs. New ArchieEnergyStorage (transfer/) mirrors ArchieItemStorage/ ArchieFluidStorage's insert/extract/simulate shape as a single capacity-capped buffer - no CommonStorage backing, since there's no cross-loader "EnergyResource" the way there is for items/fluids. Bridging to a platform energy capability (NeoForge IEnergyStorage, Fabric Team Reborn Energy) is left to the consuming mod. Also fills a pre-existing gap: NBTHolder only exposed itemField(), even though ArchieFluidStorage already existed - added fluidField()/ energyField() (all three NBTHolder implementations) so fluid/energy storage can be declared the same declarative way as item storage. Ships default "java" theme JSON + sprites for all three new composables (progress_bar/energy_bar/fluid_tank), matching the existing button/slider convention - required, not optional, since ComposableTheme throws if a composable's theme isn't registered. Verified via ./gradlew :common:compileKotlin, :fabric:compileKotlin, and :neoforge:compileKotlin (all succeed) - not run through the full GameTest suite. Co-Authored-By: Claude Sonnet 5 --- .../archie/gui/composables/basic/EnergyBar.kt | 45 +++++++ .../archie/gui/composables/basic/FluidTank.kt | 91 ++++++++++++++ .../gui/composables/basic/ProgressBar.kt | 108 ++++++++++++++++ .../gui/render/AFluidRenderPlatform.common.kt | 21 ++++ .../serialization/FluidStackNBTHolderImpl.kt | 42 +++++++ .../serialization/ItemStackNBTHolderImpl.kt | 42 +++++++ .../archie/serialization/NBTHolder.kt | 12 +- .../archie/serialization/NBTHolderImpl.kt | 70 +++++++++++ .../archie/transfer/ArchieEnergyStorage.kt | 117 ++++++++++++++++++ .../archie/archie_themes/java/energy_bar.json | 13 ++ .../archie/archie_themes/java/fluid_tank.json | 13 ++ .../archie_themes/java/progress_bar.json | 13 ++ .../textures/gui/sprites/java/energy_bar.png | Bin 0 -> 136 bytes .../gui/sprites/java/energy_bar.png.mcmeta | 10 ++ .../textures/gui/sprites/java/fluid_tank.png | Bin 0 -> 131 bytes .../gui/sprites/java/fluid_tank.png.mcmeta | 10 ++ .../gui/sprites/java/progress_bar.png | Bin 0 -> 115 bytes .../gui/sprites/java/progress_bar.png.mcmeta | 10 ++ Archie/docs/gui.md | 28 +++++ Archie/docs/serialization.md | 8 +- Archie/docs/transfer.md | 44 ++++++- .../gui/render/AFluidRenderPlatform.fabric.kt | 21 ++++ .../render/AFluidRenderPlatform.neoforge.kt | 19 +++ 23 files changed, 732 insertions(+), 5 deletions(-) create mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt create mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt create mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt create mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt create mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt create mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json create mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json create mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json create mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png create mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png.mcmeta create mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png create mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta create mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png create mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png.mcmeta create mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.fabric.kt create mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.neoforge.kt diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt new file mode 100644 index 000000000..211b48071 --- /dev/null +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt @@ -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.getAmount]). + * @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.getAmount(), storage.getCapacity(), modifier, direction, fillColor, variant) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt new file mode 100644 index 000000000..da29985a0 --- /dev/null +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt @@ -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) + } + } + }, + ) +} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt new file mode 100644 index 000000000..ba7aa018d --- /dev/null +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt @@ -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) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt new file mode 100644 index 000000000..57d3be9d5 --- /dev/null +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt @@ -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 +} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt index 1d03dd0df..39cb03450 100644 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt @@ -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 @@ -27,6 +29,8 @@ class FluidStackNBTHolderImpl(private val stack: FluidStack) : NBTHolder { private val data: MutableMap = mutableMapOf() private val itemStorage: MutableMap = mutableMapOf() + private val fluidStorage: MutableMap = mutableMapOf() + private val energyStorage: MutableMap = mutableMapOf() init { @@ -149,6 +153,28 @@ class FluidStackNBTHolderImpl(private val stack: FluidStack) : NBTHolder } } + override fun fluidField(limit: Long, size: Int): PropertyDelegateProvider> + { + 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> + { + 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) -> @@ -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) @@ -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) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt index 243917089..d88dae903 100644 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt @@ -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 kotlinx.serialization.KSerializer import kotlinx.serialization.builtins.ListSerializer @@ -24,6 +26,8 @@ class ItemStackNBTHolderImpl(private val stack: ItemStack) : NBTHolder { private val data: MutableMap = mutableMapOf() private val itemStorage: MutableMap = mutableMapOf() + private val fluidStorage: MutableMap = mutableMapOf() + private val energyStorage: MutableMap = mutableMapOf() init { @@ -146,6 +150,28 @@ class ItemStackNBTHolderImpl(private val stack: ItemStack) : NBTHolder } } + override fun fluidField(limit: Long, size: Int): PropertyDelegateProvider> + { + 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> + { + 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) -> @@ -156,6 +182,16 @@ class ItemStackNBTHolderImpl(private val stack: ItemStack) : 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) @@ -164,6 +200,12 @@ class ItemStackNBTHolderImpl(private val stack: ItemStack) : 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) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt index 8623aff26..586e6f3b5 100644 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt @@ -1,5 +1,7 @@ package net.kernelpanicsoft.archie.serialization +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 @@ -28,7 +30,9 @@ import kotlin.reflect.jvm.isAccessible * class MyBlockEntity(pos, state) : NBTBlockEntity(pos, state) { * var count by nbt.intField() * var label by nbt.stringField { "default" } - * val items by nbt.itemField(9) // 9-slot inventory + * val items by nbt.itemField(9) // 9-slot inventory + * val tank by nbt.fluidField(FluidStack.bucketAmount() * 4) // 1 tank slot, 4 buckets + * val energy by nbt.energyField(10_000) // a single energy buffer * } * ``` * @@ -51,6 +55,12 @@ interface NBTHolder /** Declares an [ArchieItemStorage] field with [size] slots, keyed by the delegated property's name. */ fun itemField(size: Int): PropertyDelegateProvider> + /** Declares an [ArchieFluidStorage] field with [size] tank slots each capped at [limit], keyed by the delegated property's name. */ + fun fluidField(limit: Long, size: Int = 1): PropertyDelegateProvider> + + /** Declares an [ArchieEnergyStorage] field capped at [capacity], keyed by the delegated property's name. */ + fun energyField(capacity: Long): PropertyDelegateProvider> + fun booleanField(default: () -> Boolean = { false }): PropertyDelegateProvider> = field(Boolean.serializer(), default) fun byteField(default: () -> Byte = { 0 }): PropertyDelegateProvider> = field(Byte.serializer(), default) fun ubyteField(default: () -> UByte = { 0u }): PropertyDelegateProvider> = field(UByte.serializer(), default) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt index e968d6dfc..5c080277c 100644 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt @@ -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 kotlinx.serialization.KSerializer import kotlinx.serialization.builtins.ListSerializer @@ -26,6 +28,8 @@ class NBTHolderImpl : NBTHolder { private val data: MutableMap = mutableMapOf() private val itemStorage: MutableMap = mutableMapOf() + private val fluidStorage: MutableMap = mutableMapOf() + private val energyStorage: MutableMap = mutableMapOf() private val sync: MutableSet = mutableSetOf() override fun field( @@ -193,6 +197,56 @@ class NBTHolderImpl : NBTHolder } } + override fun fluidField(limit: Long, size: Int): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is BlockEntity) + { + thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), ArchieFluidStorage.serializer()) + } + } + val onUpdate = when (thisRef) + { + is BlockEntity -> ({ + if (property.name.toSnakeCase() in sync) + thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), fluidStorage[property.name.toSnakeCase()]) + thisRef.setChanged() + }) + else -> ({}) + } + fluidStorage[property.name.toSnakeCase()] = ArchieFluidStorage(limit, size, onUpdate) + ReadOnlyProperty { _, _ -> fluidStorage[property.name.toSnakeCase()]!! } + } + } + + override fun energyField(capacity: Long): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is BlockEntity) + { + thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), ArchieEnergyStorage.serializer()) + } + } + val onUpdate = when (thisRef) + { + is BlockEntity -> ({ + if (property.name.toSnakeCase() in sync) + thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), energyStorage[property.name.toSnakeCase()]) + thisRef.setChanged() + }) + else -> ({}) + } + energyStorage[property.name.toSnakeCase()] = ArchieEnergyStorage(capacity, onUpdate) + ReadOnlyProperty { _, _ -> energyStorage[property.name.toSnakeCase()]!! } + } + } + override fun loadFromTag(compoundTag: CompoundTag) { forEachTag(compoundTag) { (key, value) -> @@ -203,6 +257,16 @@ class NBTHolderImpl : 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) @@ -211,6 +275,12 @@ class NBTHolderImpl : 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) } diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt new file mode 100644 index 000000000..1acaf7b20 --- /dev/null +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt @@ -0,0 +1,117 @@ +package net.kernelpanicsoft.archie.transfer + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.benwoodworth.knbt.NbtTag +import net.kernelpanicsoft.archie.serialization.NBT +import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless +import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless +import kotlin.math.min + +/** + * Archie's platform-agnostic energy buffer: a single [capacity]-capped [Long] amount, with + * resource-style [insert]/[extract] mirroring [ArchieItemStorage]/[ArchieFluidStorage]. + * + * Unlike items and fluids, there is no single cross-loader energy standard - Archie doesn't + * bridge this to a platform capability (NeoForge's `IEnergyStorage`, Fabric's Team Reborn Energy + * API) for you. Wire that yourself in loader-specific code, reading/writing through [getAmount], + * [getCapacity], [insert], and [extract]; the [net.kernelpanicsoft.archie.gui.composables.basic.EnergyBar] + * composable only needs the plain amount/capacity pair to render. + * + * Usually created through [net.kernelpanicsoft.archie.serialization.NBTHolder.energyField] + * rather than directly. + * + * @param capacity The maximum amount of energy this storage can hold. + * @param onUpdate Invoked whenever this storage's contents change, for persistence/sync. + */ +@Serializable(with = ArchieEnergyStorage.Serializer::class) +class ArchieEnergyStorage( + private var capacity: Long, + private val onUpdate: () -> Unit = {}, +) +{ + private var amount: Long = 0 + + /** The amount of energy currently stored. */ + fun getAmount(): Long = amount + + /** The maximum amount of energy this storage can hold. */ + fun getCapacity(): Long = capacity + + /** + * Inserts up to [amount] energy, returning how much was actually accepted. + * When [simulate] is `true`, no state is changed - only the acceptable amount is calculated. + */ + fun insert(amount: Long, simulate: Boolean): Long + { + val inserted = min(amount, capacity - this.amount) + if (inserted <= 0) return 0 + if (!simulate) + { + this.amount += inserted + onUpdate() + } + return inserted + } + + /** + * Extracts up to [amount] energy, returning how much was actually removed. + * When [simulate] is `true`, no state is changed - only the extractable amount is calculated. + */ + fun extract(amount: Long, simulate: Boolean): Long + { + val extracted = min(amount, this.amount) + if (extracted <= 0) return 0 + if (!simulate) + { + this.amount -= extracted + onUpdate() + } + return extracted + } + + /** Directly overwrites the stored amount, clamped to `0..`[capacity]. */ + fun set(amount: Long) + { + this.amount = amount.coerceIn(0, capacity) + onUpdate() + } + + /** Snapshots this storage's [capacity] and stored amount as an [NbtTag], for save/sync. */ + fun createSnapshot(): NbtTag = NBT.encodeToNbtTagRootless(serializer(), this) + + /** Restores this storage's [capacity] and stored amount from a snapshot produced by [createSnapshot]. */ + fun readSnapshot(snapshot: NbtTag) + { + val decoded = NBT.decodeFromNbtTagRootless(serializer(), snapshot) + this.capacity = decoded.capacity + this.amount = decoded.amount + } + + /** Serializes an [ArchieEnergyStorage] as its [capacity] followed by its stored amount. */ + object Serializer : KSerializer + { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ArchieEnergyStorage") { + element("capacity", Long.serializer().descriptor) + element("amount", Long.serializer().descriptor) + } + + override fun deserialize(decoder: Decoder): ArchieEnergyStorage + { + val capacity = decoder.decodeLong() + val amount = decoder.decodeLong() + return ArchieEnergyStorage(capacity).also { it.amount = amount } + } + + override fun serialize(encoder: Encoder, value: ArchieEnergyStorage) + { + encoder.encodeLong(value.capacity) + encoder.encodeLong(value.amount) + } + } +} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json new file mode 100644 index 000000000..be64d72bc --- /dev/null +++ b/Archie/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json @@ -0,0 +1,13 @@ +{ + "states": { + "default": { + "texture": "archie:java/energy_bar", + "texture_size": { + "width": 32, + "height": 16 + }, + "width": 32, + "height": 16 + } + } +} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json new file mode 100644 index 000000000..931ca1fe0 --- /dev/null +++ b/Archie/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json @@ -0,0 +1,13 @@ +{ + "states": { + "default": { + "texture": "archie:java/fluid_tank", + "texture_size": { + "width": 18, + "height": 54 + }, + "width": 18, + "height": 54 + } + } +} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json new file mode 100644 index 000000000..b9ab69729 --- /dev/null +++ b/Archie/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json @@ -0,0 +1,13 @@ +{ + "states": { + "default": { + "texture": "archie:java/progress_bar", + "texture_size": { + "width": 32, + "height": 16 + }, + "width": 32, + "height": 16 + } + } +} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png new file mode 100644 index 0000000000000000000000000000000000000000..eca5f3cf91876a83f7abd4799ebd03323f077ad1 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Et!3HGD8EPYel)tBoV@SoEWD!BZpXUu48af*r z8xynFiQLs~yLpm>o6(R_ZkN@AtL^$cJUkZ!Bs>gWH3YKvSS4IxT+Ek{n9vZyb8qte ji&v9o%kii%A68RMSgV%3}$u25n UZ{IoLKgd)DPgg&ebxsLQ00)aIl>h($ literal 0 HcmV?d00001 diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta new file mode 100644 index 000000000..ea42a1b16 --- /dev/null +++ b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 18, + "height": 54, + "border": 2 + } + } +} \ No newline at end of file diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png new file mode 100644 index 0000000000000000000000000000000000000000..98e37d595bde493d362f6c34553f9933d4a6b226 GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Et!3HGD8EPYel$EE8V@SoEWD!BZpXUu48Zs4h z(-VN8(eTQZD=)G>u8rO` and +Archie's NBT serialization, following the exact same shape: + +```kotlin +class MyBlockEntity(pos, state) : NBTBlockEntity(TYPE, pos, state) { + val tank by nbt.fluidField(FluidStack.bucketAmount() * 4) // 1 tank slot, 4 buckets +} +``` + +Display it in a screen with the [`FluidTank`](gui.md#progress-energy-and-fluid-indicators) +composable, which renders the real fluid texture and tint (not a placeholder color) via a +per-loader render bridge — Fabric's `FluidRenderHandlerRegistry` and NeoForge's +`IClientFluidTypeExtensions` expose a fluid's client appearance through unrelated APIs, so this +one spot needed platform-specific code rather than a single cross-loader call. + +--- + +## ArchieEnergyStorage + +A single `capacity`-capped `Long` buffer with resource-style `insert`/`extract`, mirroring +`ArchieItemStorage`/`ArchieFluidStorage`'s shape but without a Common Storage Lib backing — +there's no cross-loader "EnergyResource" the way there is for items and fluids, so +`ArchieEnergyStorage` doesn't implement `CommonStorage` and Archie doesn't bridge it to a +platform capability (NeoForge's `IEnergyStorage`, Fabric's Team Reborn Energy API) for you. Wire +that yourself in loader-specific code, reading/writing through `getAmount()`/`getCapacity()`/ +`insert()`/`extract()`. + +```kotlin +class MyBlockEntity(pos, state) : NBTBlockEntity(TYPE, pos, state) { + val energy by nbt.energyField(10_000) +} + +val accepted = tile.energy.insert(amount = 500, simulate = false) +``` + +Display it with the [`EnergyBar`](gui.md#progress-energy-and-fluid-indicators) composable: +`EnergyBar(storage = tile.energy)`. --- diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.fabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.fabric.kt new file mode 100644 index 000000000..27f01b376 --- /dev/null +++ b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.fabric.kt @@ -0,0 +1,21 @@ +package net.kernelpanicsoft.archie.gui.render + +import net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandlerRegistry +import net.minecraft.client.renderer.texture.TextureAtlasSprite +import net.minecraft.world.level.material.Fluid + +/** Fabric implementation of [AFluidRenderPlatform], backed by `fabric-rendering-fluids-v1`. */ +actual object AFluidRenderPlatform +{ + actual fun getStillSprite(fluid: Fluid): TextureAtlasSprite? + { + val handler = FluidRenderHandlerRegistry.INSTANCE.get(fluid) ?: return null + return handler.getFluidSprites(null, null, fluid.defaultFluidState()).getOrNull(0) + } + + actual fun getTintColor(fluid: Fluid): Int + { + val handler = FluidRenderHandlerRegistry.INSTANCE.get(fluid) ?: return -1 + return handler.getFluidColor(null, null, fluid.defaultFluidState()) + } +} diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.neoforge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.neoforge.kt new file mode 100644 index 000000000..0e973b4a0 --- /dev/null +++ b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.neoforge.kt @@ -0,0 +1,19 @@ +package net.kernelpanicsoft.archie.gui.render + +import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.texture.TextureAtlas +import net.minecraft.client.renderer.texture.TextureAtlasSprite +import net.minecraft.world.level.material.Fluid +import net.neoforged.neoforge.client.extensions.common.IClientFluidTypeExtensions + +/** NeoForge implementation of [AFluidRenderPlatform], backed by [IClientFluidTypeExtensions]. */ +actual object AFluidRenderPlatform +{ + actual fun getStillSprite(fluid: Fluid): TextureAtlasSprite? + { + val loc = IClientFluidTypeExtensions.of(fluid).stillTexture ?: return null + return Minecraft.getInstance().modelManager.getAtlas(TextureAtlas.LOCATION_BLOCKS).getSprite(loc) + } + + actual fun getTintColor(fluid: Fluid): Int = IClientFluidTypeExtensions.of(fluid).tintColor +} From 6523daa77ba707c7374962af965f3c0285a863db Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Tue, 4 Aug 2026 12:20:54 -0400 Subject: [PATCH 2/2] Fix ArchieEnergyStorage to implement Common Storage Lib's ValueStorage Common Storage Lib (already an Archie dependency) already ships an energy API - earth.terrarium.common_storage_lib.energy.EnergyApi, EnergyProvider, and the storage.base.ValueStorage interface - that the previous commit missed, incorrectly claiming "there's no cross-loader EnergyResource the way there is for items/fluids" and building a standalone class instead. ArchieEnergyStorage now implements ValueStorage + UpdateManager the same way ArchieItemStorage/ArchieFluidStorage implement CommonStorage + UpdateManager, so it's consistent with the rest of transfer/ and interoperable with anything else built against CSL's ValueStorage. getAmount() is renamed to getStoredAmount() to match the interface's real method name. Archie still doesn't auto-register with EnergyApi.BLOCK/ITEM/ENTITY, matching the current (also unregistered) state of item/fluid storage - that's left as consistent future work for all three, not something to bolt onto energy alone in this PR. Co-Authored-By: Claude Sonnet 5 --- .../archie/gui/composables/basic/EnergyBar.kt | 4 +- .../archie/transfer/ArchieEnergyStorage.kt | 51 +++++++++++-------- Archie/docs/transfer.md | 17 ++++--- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt index 211b48071..9259def4d 100644 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt @@ -12,7 +12,7 @@ import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage * 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.getAmount]). + * @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. @@ -42,4 +42,4 @@ fun EnergyBar( direction: ProgressDirection = ProgressDirection.BOTTOM_TO_TOP, fillColor: Int = 0xFFFF5C33.toInt(), variant: String = ThemeVariants.DEFAULT, -) = EnergyBar(storage.getAmount(), storage.getCapacity(), modifier, direction, fillColor, variant) +) = EnergyBar(storage.getStoredAmount(), storage.getCapacity(), modifier, direction, fillColor, variant) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt index 1acaf7b20..76b6a42a3 100644 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt +++ b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt @@ -1,5 +1,7 @@ package net.kernelpanicsoft.archie.transfer +import earth.terrarium.common_storage_lib.storage.base.UpdateManager +import earth.terrarium.common_storage_lib.storage.base.ValueStorage import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.serializer @@ -14,14 +16,15 @@ import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless import kotlin.math.min /** - * Archie's platform-agnostic energy buffer: a single [capacity]-capped [Long] amount, with - * resource-style [insert]/[extract] mirroring [ArchieItemStorage]/[ArchieFluidStorage]. + * Archie's platform-agnostic energy buffer: implements Common Storage Lib's [ValueStorage] - + * the energy analogue of the `CommonStorage`/`CommonStorage` + * [ArchieItemStorage]/[ArchieFluidStorage] implement - plus Archie's NBT serialization for + * save/load, mirroring their shape. * - * Unlike items and fluids, there is no single cross-loader energy standard - Archie doesn't - * bridge this to a platform capability (NeoForge's `IEnergyStorage`, Fabric's Team Reborn Energy - * API) for you. Wire that yourself in loader-specific code, reading/writing through [getAmount], - * [getCapacity], [insert], and [extract]; the [net.kernelpanicsoft.archie.gui.composables.basic.EnergyBar] - * composable only needs the plain amount/capacity pair to render. + * Archie doesn't register this with Common Storage Lib's `EnergyApi.BLOCK`/`ITEM`/`ENTITY` + * lookups for you, the same way it doesn't for `ArchieItemStorage`/`ArchieFluidStorage` today - + * wire that lookup registration, and any platform-specific capability bridge (NeoForge's + * `IEnergyStorage`, Fabric's Team Reborn Energy API) you still want on top of it, in your own mod. * * Usually created through [net.kernelpanicsoft.archie.serialization.NBTHolder.energyField] * rather than directly. @@ -33,28 +36,32 @@ import kotlin.math.min class ArchieEnergyStorage( private var capacity: Long, private val onUpdate: () -> Unit = {}, -) +) : ValueStorage, UpdateManager { private var amount: Long = 0 /** The amount of energy currently stored. */ - fun getAmount(): Long = amount + override fun getStoredAmount(): Long = amount /** The maximum amount of energy this storage can hold. */ - fun getCapacity(): Long = capacity + override fun getCapacity(): Long = capacity + + override fun allowsInsertion(): Boolean = true + + override fun allowsExtraction(): Boolean = true /** * Inserts up to [amount] energy, returning how much was actually accepted. * When [simulate] is `true`, no state is changed - only the acceptable amount is calculated. */ - fun insert(amount: Long, simulate: Boolean): Long + override fun insert(amount: Long, simulate: Boolean): Long { val inserted = min(amount, capacity - this.amount) if (inserted <= 0) return 0 if (!simulate) { this.amount += inserted - onUpdate() + update() } return inserted } @@ -63,37 +70,39 @@ class ArchieEnergyStorage( * Extracts up to [amount] energy, returning how much was actually removed. * When [simulate] is `true`, no state is changed - only the extractable amount is calculated. */ - fun extract(amount: Long, simulate: Boolean): Long + override fun extract(amount: Long, simulate: Boolean): Long { val extracted = min(amount, this.amount) if (extracted <= 0) return 0 if (!simulate) { this.amount -= extracted - onUpdate() + update() } return extracted } - /** Directly overwrites the stored amount, clamped to `0..`[capacity]. */ + /** Directly overwrites the stored amount, clamped to `0..`[getCapacity]. */ fun set(amount: Long) { this.amount = amount.coerceIn(0, capacity) - onUpdate() + update() } - /** Snapshots this storage's [capacity] and stored amount as an [NbtTag], for save/sync. */ - fun createSnapshot(): NbtTag = NBT.encodeToNbtTagRootless(serializer(), this) + /** Snapshots this storage's [getCapacity] and stored amount as an [NbtTag], for save/sync. */ + override fun createSnapshot(): NbtTag = NBT.encodeToNbtTagRootless(serializer(), this) - /** Restores this storage's [capacity] and stored amount from a snapshot produced by [createSnapshot]. */ - fun readSnapshot(snapshot: NbtTag) + /** Restores this storage's capacity and stored amount from a snapshot produced by [createSnapshot]. */ + override fun readSnapshot(snapshot: NbtTag) { val decoded = NBT.decodeFromNbtTagRootless(serializer(), snapshot) this.capacity = decoded.capacity this.amount = decoded.amount } - /** Serializes an [ArchieEnergyStorage] as its [capacity] followed by its stored amount. */ + override fun update() = onUpdate() + + /** Serializes an [ArchieEnergyStorage] as its capacity followed by its stored amount. */ object Serializer : KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ArchieEnergyStorage") { diff --git a/Archie/docs/transfer.md b/Archie/docs/transfer.md index b1b57ef4f..142778286 100644 --- a/Archie/docs/transfer.md +++ b/Archie/docs/transfer.md @@ -49,13 +49,16 @@ one spot needed platform-specific code rather than a single cross-loader call. ## ArchieEnergyStorage -A single `capacity`-capped `Long` buffer with resource-style `insert`/`extract`, mirroring -`ArchieItemStorage`/`ArchieFluidStorage`'s shape but without a Common Storage Lib backing — -there's no cross-loader "EnergyResource" the way there is for items and fluids, so -`ArchieEnergyStorage` doesn't implement `CommonStorage` and Archie doesn't bridge it to a -platform capability (NeoForge's `IEnergyStorage`, Fabric's Team Reborn Energy API) for you. Wire -that yourself in loader-specific code, reading/writing through `getAmount()`/`getCapacity()`/ -`insert()`/`extract()`. +Implements Common Storage Lib's `ValueStorage` — the energy analogue of the +`CommonStorage`/`CommonStorage` `ArchieItemStorage`/ +`ArchieFluidStorage` implement — plus Archie's NBT serialization, mirroring their shape: +a `capacity`-capped `Long` buffer with resource-style `insert`/`extract`. + +Archie doesn't register this with Common Storage Lib's `EnergyApi.BLOCK`/`ITEM`/`ENTITY` lookups +for you, the same way it doesn't for `ArchieItemStorage`/`ArchieFluidStorage` today — wire that +lookup registration, and any platform-specific capability bridge (NeoForge's `IEnergyStorage`, +Fabric's Team Reborn Energy API) you still want on top of it, in your own mod. Read/write through +`getStoredAmount()`/`getCapacity()`/`insert()`/`extract()`. ```kotlin class MyBlockEntity(pos, state) : NBTBlockEntity(TYPE, pos, state) {